diff --git a/api/src/main/java/org/apache/brooklyn/api/entity/EntityInitializer.java b/api/src/main/java/org/apache/brooklyn/api/entity/EntityInitializer.java index a9f407ab43..c138971a6c 100644 --- a/api/src/main/java/org/apache/brooklyn/api/entity/EntityInitializer.java +++ b/api/src/main/java/org/apache/brooklyn/api/entity/EntityInitializer.java @@ -23,19 +23,26 @@ import org.apache.brooklyn.api.objs.EntityAdjunct; import org.apache.brooklyn.api.policy.Policy; import org.apache.brooklyn.api.sensor.Feed; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlSingletonMap; /** * Instances of this class supply logic which can be used to initialize entities. * These can be added to an {@link EntitySpec} programmatically, or declared as part * of YAML recipes in a brooklyn.initializers section. - * In the case of the latter, implementing classes should define a no-arg constructor - * or a {@link Map} constructor so that YAML parameters can be supplied. + *

+ * When intended for use from YAML, implementing classes should define + * a single-argument constructor taking either a {@link Map} or a ConfigBag + * so that YAML parameters can be supplied + * (or a no-arg constructor if parameters are not supported). *

* Note that initializers are only invoked on first creation; they are not called * during a rebind. Instead, the typical pattern is that initializers will create * {@link EntityAdjunct} instances such as {@link Policy} and {@link Feed} * which will be attached during rebind. **/ +@YomlSingletonMap(keyForPrimitiveValue="type") +@Alias("entity-initializer") public interface EntityInitializer { /** Applies initialization logic to a just-built entity. diff --git a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java index 925b6b1a5d..af936662b3 100644 --- a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java +++ b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredTypeLoadingContext.java @@ -27,6 +27,7 @@ import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; public interface RegisteredTypeLoadingContext { @@ -37,6 +38,7 @@ public interface RegisteredTypeLoadingContext { * for specs, this refers to the target type, not the spec * (eg {@link Entity} not {@link EntitySpec}). * If nothing is specified, this returns {@link Object}'s class. */ + // TODO extend to offer expected registered super type @Nonnull public Class getExpectedJavaSuperType(); /** encountered types, so that during resolution, @@ -47,4 +49,9 @@ public interface RegisteredTypeLoadingContext { /** A loader to use, supplying preferred or additional bundles and search paths */ @Nullable public BrooklynClassLoadingContext getLoader(); + /** Optional instructions on how to invoke the constructor, + * for use when a caller needs to specify a special constructor or factory method + * and/or specify parameters */ + @Nullable public ConstructionInstruction getConstructorInstruction(); + } diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityDecorationResolver.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityDecorationResolver.java index 181fa67ae2..6aba7463a6 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityDecorationResolver.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityDecorationResolver.java @@ -35,12 +35,15 @@ import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.camp.brooklyn.BrooklynCampReservedKeys; import org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynYamlTypeInstantiator.InstantiatorFromKey; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon; import org.apache.brooklyn.core.entity.BrooklynConfigKeys; import org.apache.brooklyn.core.mgmt.BrooklynTags; import org.apache.brooklyn.core.objs.BasicSpecParameter; import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.core.typereg.RegisteredTypes; import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.core.task.DeferredSupplier; import org.apache.brooklyn.util.guava.Maybe; @@ -77,13 +80,18 @@ protected List buildListOfTheseDecorationsFromEntityAttributes(Con if (value==null) { return MutableList.of(); } if (value instanceof Iterable) { return buildListOfTheseDecorationsFromIterable((Iterable)value); + } else if (canBuildFromMap()) { + if (value instanceof Map) { + return buildListOfTheseDecorationsFromMap((Map)value); + } else { + throw new IllegalArgumentException(getDecorationKind()+" body should be map or iterable, not " + value.getClass()); + } } else { - // in future may support types other than iterables here, - // e.g. a map short form where the key is the type throw new IllegalArgumentException(getDecorationKind()+" body should be iterable, not " + value.getClass()); } } + protected Map checkIsMap(Object decorationJson) { if (!(decorationJson instanceof Map)) { throw new IllegalArgumentException(getDecorationKind()+" value must be a Map, not " + @@ -100,6 +108,11 @@ protected List

buildListOfTheseDecorationsFromIterable(Iterable value) { return decorations; } + // optional if syntax supports a map input + // (e.g. where the key is the type or the name) + protected boolean canBuildFromMap() { return false; } + protected List
buildListOfTheseDecorationsFromMap(Map value) { throw new UnsupportedOperationException(); } + protected abstract String getDecorationKind(); protected abstract Object getDecorationAttributeJsonValue(ConfigBag attrs); @@ -201,6 +214,22 @@ protected Object getDecorationAttributeJsonValue(ConfigBag attrs) { protected void addDecorationFromJsonMap(Map decorationJson, List decorations) { decorations.add(instantiator.from(decorationJson).prefix("initializer").newInstance(EntityInitializer.class)); } + + @Override + protected boolean canBuildFromMap() { + return true; + } + + @Override + protected List buildListOfTheseDecorationsFromMap(Map value) { + ManagementContext mgmt = instantiator.loader.getManagementContext(); + List result = MutableList.of(); + for (Map.Entry v: value.entrySet()) { + result.add(mgmt.getTypeRegistry().createBeanFromPlan("yoml", MutableMap.of(v.getKey(), v.getValue()), + RegisteredTypeLoadingContexts.loader(instantiator.loader), EntityInitializer.class)); + } + return result; + } } // Not much value from extending from BrooklynEntityDecorationResolver, but let's not break the convention diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityMatcher.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityMatcher.java index ddfdd93a7e..62d23ca17e 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityMatcher.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynEntityMatcher.java @@ -126,14 +126,14 @@ public boolean apply(Object deploymentPlanItem, AssemblyTemplateConstructor atc) brooklynFlags.putAll((Map)origBrooklynFlags); } - addCustomMapAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CONFIG); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_POLICIES); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_ENRICHERS); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_INITIALIZERS); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CHILDREN); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_PARAMETERS); - addCustomMapAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CATALOG); - addCustomListAttributeIfNonNull(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_TAGS); + addCustomMapAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CONFIG); + addCustomListAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_POLICIES); + addCustomListAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_ENRICHERS); + addCustomAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_INITIALIZERS, true, true); + addCustomListAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CHILDREN); + addCustomListAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_PARAMETERS); + addCustomMapAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_CATALOG); + addCustomListAttributeIfNonEmpty(builder, attrs, BrooklynCampReservedKeys.BROOKLYN_TAGS); brooklynFlags.putAll(attrs); if (!brooklynFlags.isEmpty()) { @@ -150,18 +150,8 @@ public boolean apply(Object deploymentPlanItem, AssemblyTemplateConstructor atc) * as a custom attribute with type List. * @throws java.lang.IllegalArgumentException if map[key] is not an instance of List */ - private void addCustomListAttributeIfNonNull(Builder builder, Map attrs, String key) { - Object items = attrs.remove(key); - if (items != null) { - if (items instanceof List) { - List itemList = (List) items; - if (!itemList.isEmpty()) { - builder.customAttribute(key, Lists.newArrayList(itemList)); - } - } else { - throw new IllegalArgumentException(key + " must be a list, is: " + items.getClass().getName()); - } - } + private void addCustomListAttributeIfNonEmpty(Builder builder, Map attrs, String key) { + addCustomAttributeIfNonEmpty(builder, attrs, key, false, true); } /** @@ -169,16 +159,28 @@ private void addCustomListAttributeIfNonNull(Builder builder, Map attrs, String key) { + private void addCustomMapAttributeIfNonEmpty(Builder builder, Map attrs, String key) { + addCustomAttributeIfNonEmpty(builder, attrs, key, true, false); + } + + private void addCustomAttributeIfNonEmpty(Builder builder, Map attrs, String key, + boolean allowMap, boolean allowList) { Object items = attrs.remove(key); if (items != null) { - if (items instanceof Map) { + if (allowMap && items instanceof Map) { Map itemMap = (Map) items; if (!itemMap.isEmpty()) { builder.customAttribute(key, Maps.newHashMap(itemMap)); } + } else if (allowList && items instanceof List) { + List itemList = (List) items; + if (!itemList.isEmpty()) { + builder.customAttribute(key, Lists.newArrayList(itemList)); + } } else { - throw new IllegalArgumentException(key + " must be a map, is: " + items.getClass().getName()); + throw new IllegalArgumentException(key + " must be a "+ + (allowMap && allowList ? "map or list" : allowMap ? "map" : allowList ? "list" : "")+ + ", is: " + items.getClass().getName()); } } } diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/CampTypePlanTransformer.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/CampTypePlanTransformer.java index eaa68b898c..2cf3285dcb 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/CampTypePlanTransformer.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/CampTypePlanTransformer.java @@ -22,14 +22,16 @@ import java.util.Map; import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.api.typereg.RegisteredType.TypeImplementationPlan; import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; -import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; import org.apache.brooklyn.core.typereg.AbstractFormatSpecificTypeImplementationPlan; import org.apache.brooklyn.core.typereg.AbstractTypePlanTransformer; import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan; +import org.apache.brooklyn.core.typereg.RegisteredTypeInfo; import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.guava.Maybe; import com.google.common.collect.ImmutableList; @@ -84,28 +86,24 @@ protected double scoreForNonmatchingNonnullFormat(String planFormat, Object plan @Override protected AbstractBrooklynObjectSpec createSpec(RegisteredType type, RegisteredTypeLoadingContext context) throws Exception { - // TODO cache + // could cache and copy each time, if this bit is slow + // (but don't think it is now) return new CampResolver(mgmt, type, context).createSpec(); } @Override protected Object createBean(RegisteredType type, RegisteredTypeLoadingContext context) throws Exception { - // beans not supported by this? + // beans not supported by this? - want YOML for this throw new IllegalStateException("beans not supported here"); } @Override - public double scoreForTypeDefinition(String formatCode, Object catalogData) { - // TODO catalog parsing - return 0; - } - - @Override - public List createFromTypeDefinition(String formatCode, Object catalogData) { - // TODO catalog parsing - return null; + public RegisteredTypeInfo getTypeInfo(RegisteredType type) { + // TODO collect immediate supertypes, as RegisteredType and Class instances + // we really want YOML for this however + return RegisteredTypeInfo.create(type, this, null, MutableSet.of()); } - + public static class CampTypeImplementationPlan extends AbstractFormatSpecificTypeImplementationPlan { public CampTypeImplementationPlan(TypeImplementationPlan otherPlan) { super(FORMATS.get(0), String.class, otherPlan); diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java index 4105e19cb3..896bc17cc3 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java @@ -36,6 +36,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.CaseFormat; + /** * {@link PlanInterpreter} which understands the $brooklyn DSL */ @@ -156,6 +158,9 @@ public Object evaluateOn(Object o, FunctionWithArgs f, boolean deepEvaluation) { String fn = f.getFunction(); fn = Strings.removeFromStart(fn, BrooklynDslCommon.PREFIX); + if (fn.contains("-")) { + fn = CaseFormat.LOWER_HYPHEN.to(CaseFormat.LOWER_CAMEL, fn); + } if (fn.startsWith("function.")) { // If the function name starts with 'function.', then we look for the function in BrooklynDslCommon.Functions // As all functions in BrooklynDslCommon.Functions are static, we don't need to worry whether a class diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java index e265e84161..6a793f0ef4 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java @@ -30,6 +30,7 @@ import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ExecutionContext; +import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.api.objs.Configurable; import org.apache.brooklyn.api.sensor.Sensor; @@ -40,6 +41,7 @@ import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope; import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; import org.apache.brooklyn.core.entity.EntityDynamicType; @@ -51,6 +53,7 @@ import org.apache.brooklyn.core.objs.AbstractConfigurationSupportInternal; import org.apache.brooklyn.core.objs.BrooklynObjectInternal; import org.apache.brooklyn.core.sensor.DependentConfiguration; +import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ClassLoaderUtils; @@ -66,6 +69,7 @@ import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; import org.apache.brooklyn.util.net.Urls; +import org.apache.brooklyn.util.text.Strings; import org.apache.commons.beanutils.BeanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -253,6 +257,7 @@ public static Sensor sensor(String clazzName, String sensorName) { try { // TODO Should use catalog's classloader, rather than ClassLoaderUtils; how to get that? Should we return a future?! // Should have the catalog's loader at this point in a thread local + // eg CatalogUtils.getClassLoadingContext(entity) String mappedClazzName = DeserializingClassRenamesProvider.INSTANCE.findMappedName(clazzName); Class clazz = new ClassLoaderUtils(BrooklynDslCommon.class).loadClass(mappedClazzName); @@ -323,6 +328,10 @@ public static Object object(Map arguments) { } } } + + public static Object objectYoml(Object parseTree) { + return new DslYomlObject(parseTree); + } // String manipulation @@ -590,7 +599,6 @@ public DslObject( this.config = MutableMap.copyOf(config); } - @Override public Maybe getImmediately() { final Class clazz = getOrLoadType(); @@ -662,13 +670,18 @@ public Object call() throws Exception { protected Class getOrLoadType() { Class type = this.type; - if (type == null) { - EntityInternal entity = entity(); - try { - type = new ClassLoaderUtils(BrooklynDslCommon.class, entity).loadClass(typeName); - } catch (ClassNotFoundException e) { - throw Exceptions.propagate(e); - } + if (type != null) return type; + + EntityInternal entity = entity(); + Maybe> typeM = CatalogUtils.getClassLoadingContext(entity).tryLoadClass(typeName); + + if (typeM.isPresent()) return typeM.get(); + + try { + type = new ClassLoaderUtils(BrooklynDslCommon.class, entity).loadClass(typeName); + } catch (ClassNotFoundException e) { + // prefer the exception from typeM + typeM.get(); // (will always throw) } return type; } @@ -737,6 +750,68 @@ public String toString() { } } + /** Deferred execution of Object creation from YOML input. Note no expected type can be set currently, + * and none is inferred. */ + protected static class DslYomlObject extends BrooklynDslDeferredSupplier { + + private static final long serialVersionUID = 8878388748085419L; + + private final Object parseTree; + + public DslYomlObject(Object parseTree) { + this.parseTree = parseTree; + } + + @Override + public Maybe getImmediately() { + final ExecutionContext executionContext = ((EntityInternal)entity()).getExecutionContext(); + Maybe parseTreeResolved = Tasks.resolving(parseTree, Object.class).context(executionContext).deep(true).immediately(true).getMaybe(); + if (parseTreeResolved.isAbsent()) return parseTreeResolved; + return Maybe.of(create(entity().getManagementContext(), entity(), parseTreeResolved.get())); + } + + @Override + public Task newTask() { + return Tasks.builder().displayName("Instantiating yoml supplied in DSL") + .tag(BrooklynTaskTags.TRANSIENT_TASK_TAG) + .dynamic(false) + .body(new Callable() { + @Override + public Object call() throws Exception { + return create(entity().getManagementContext(), entity(), parseTree); + }}) + .build(); + } + + public static Object create(ManagementContext mgmt, Entity entity, Object parseTree) { + return mgmt.getTypeRegistry().createBeanFromPlan("yoml", parseTree, + RegisteredTypeLoadingContexts.loader(CatalogUtils.getClassLoadingContext(entity())), null); + } + + @Override + public int hashCode() { + return Objects.hashCode(parseTree); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + DslYomlObject that = DslYomlObject.class.cast(obj); + return Objects.equal(this.parseTree, that.parseTree); + } + + @Override + public String toString() { + // TODO this may not generate reparseable output for complex maps; + // ideally we'd have access to the original parse node and contents and could embed that here, + // but that requires refactoring the parser. + // (otoh that would also allow us to pass the string plan as is preferred in YomlTypePlanTransformer.) + return DslToStringHelpers.fn("object-yoml", Strings.toString(parseTree)); + } + + } + /** * Defers to management context's {@link ExternalConfigSupplierRegistry} to resolve values at runtime. * The name of the appropriate {@link ExternalConfigSupplier} is captured, along with the key of @@ -827,7 +902,6 @@ public RegexReplacer(String pattern, String replacement) { protected static class DslRegexReplacer extends BrooklynDslDeferredSupplier> { private static final long serialVersionUID = -2900037495440842269L; - private Object pattern; private Object replacement; diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java index 7b0f359dc6..3eaa08bdb5 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/parse/DslParser.java @@ -78,7 +78,7 @@ public Object next() { char c = expression.charAt(index); if (Character.isJavaIdentifierPart(c)) ; // these chars also permitted - else if (".:".indexOf(c)>=0) ; + else if (".:-".indexOf(c)>=0) ; // other things e.g. whitespace, parentheses, etc, skip else break; index++; diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlAnnotations.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlAnnotations.java new file mode 100644 index 0000000000..c025c5e48c --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlAnnotations.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import org.apache.brooklyn.camp.yoml.serializers.InstantiateTypeFromRegistryUsingConfigBag; +import org.apache.brooklyn.camp.yoml.serializers.InstantiateTypeFromRegistryUsingConfigKeyMap; +import org.apache.brooklyn.util.core.yoml.YomlConfigBagConstructor; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.YomlAnnotations; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; + +public class BrooklynYomlAnnotations extends YomlAnnotations { + + public Collection findConfigBagConstructorSerializers(Class t) { + YomlConfigBagConstructor ann = t.getAnnotation(YomlConfigBagConstructor.class); + if (ann==null) return Collections.emptyList(); + return new InstantiateTypeFromRegistryUsingConfigBag.Factory().newConfigKeySerializersForType( + t, + ann.value(), ann.writeAsKey()!=null ? ann.writeAsKey() : ann.value(), + ann.validateAheadOfTime(), ann.requireStaticKeys()); + } + + public Collection findConfigMapConstructorSerializersIgnoringConfigInheritance(Class t) { + throw new UnsupportedOperationException("ensure this doesn't get called"); + } + + public Collection findConfigMapConstructorSerializersWithInheritance(Class t) { + YomlConfigMapConstructor ann = t.getAnnotation(YomlConfigMapConstructor.class); + if (ann==null) return Collections.emptyList(); + return new InstantiateTypeFromRegistryUsingConfigKeyMap.Factory().newConfigKeySerializersForType( + t, + ann.value(), Strings.isNonBlank(ann.writeAsKey()) ? ann.writeAsKey() : ann.value(), + ann.validateAheadOfTime(), ann.requireStaticKeys()); + } + + @Override + protected void collectSerializersForConfig(Set result, Class type) { + result.addAll(findConfigBagConstructorSerializers(type)); + result.addAll(findConfigMapConstructorSerializersWithInheritance(type)); + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTypeRegistry.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTypeRegistry.java new file mode 100644 index 0000000000..291e27764a --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTypeRegistry.java @@ -0,0 +1,532 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.api.typereg.RegisteredType.TypeImplementationPlan; +import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; +import org.apache.brooklyn.camp.yoml.YomlTypePlanTransformer.YomlTypeImplementationPlan; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext; +import org.apache.brooklyn.core.typereg.BasicRegisteredType; +import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; +import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.YomlTypeRegistry; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.reflect.TypeToken; + +/** + * Provides a bridge so YOML's type registry can find things in the Brooklyn type registry. + *

+ * There are several subtleties in the loading strategy this registry should use depending + * when YOML is asking for something to be loaded. This is done through various + * {@link RegisteredTypeLoadingContext} instances. That class is able to do things including: + * + * (a) specify a supertype to use for filtering when finding a type + * (b) specify a class loading context (eg based on context's catalog item id / definition) + * (c) specify a set of encountered types to prevent looping and resolve a duplicate name + * as a class if it has already been resolved as a YOML item + * (eg yoml item java.pack.Foo declares its type as java.pack.Foo to mean to load it as a class) + * or simply bail out if there is a recursive definition (eg require java:java.pack.Foo in the above case) + * (d) specify a construction instruction specified by wrapper/subtype definitions + *

+ * Which of these should apply depends on the calling context. The following situations apply: + * + * (1) when YOML makes the first call to resolve the type at "/", we should apply (a) and (b) supplied by the user; + * (c) and (d) should be empty but no harm in applying them (and it will make recursive work easier) + * (2) if in the course of that call this registry calls to YOML to evaluate a plan definition of a supertype, + * it should act like (1) except use the supertype's loader; ie apply everything except (b) + * (3) if YOML makes a subsequent call to resolve a type at a different path, it should apply only (b), + * not any of the others + *

+ * See {@link #getTypeContextFor(YomlContext)} and usages. + * + *

+ * + * More details on library loading. Consider for instance: + * + * - id: x + * item: { type: X } + * - id: x2 + * item: { type: x } + * - id: cluster-x + * item: { type: cluster, children: [ { type: x }, { type: X } ] } + * + * And assume these items declare different libraries. + * + * We *need* libraries to be used when resolving the reference to a parent java type (e.g. x's parent type X). + * + * We *don't* want libraries to be used transitively, e.g. when x2 or cluster-x refers to x, + * only x's libraries apply to loading X, not x2's libraries. + * + * We *may* want libraries to be used when resolving references to types in a plan besides the parent type; + * e.g. cluster-x's libraries will be needed when resolving its reference to it's child of declared type X. + * But we might *NOT* want to support that, and could instead require that any type referenced elsewhere + * be defined as an explicit YAML type in the registry. This will simplify our lives as we will force all + * java objects to be explicitly defined as a type in the registry. But it means we won't be able to parse + * current plans so for the moment this is deferred, and we'd want to go through a "warning" cycle when + * applying. + * + * (In that last case we could even be stricter and say that any yaml types + * should have a simple single yaml-java bridge eg using a JavaClassNameTypeImplementationPlan + * to facilitate reverse lookup.) + * + * The defaultLoadingContext is used for the first and third cases above. + * The second case is handled by calling back to the registry with a limited context. + * (Note it will require some work to be able to distinguish between the third case and the first, + * as we don't currently have that contextual information when methods here are called.) + * + *

+ * + * One bit of ugly remains: + * + * If we install v1 then v2, cluster-x:1 will pick up x:2. + * We could change this: + * - by encouraging explicit `type: x:1` in the definition of cluster-x + * - by allowing `type: x:.` version shorthand, where `.` means the same version as the calling type + * - by recording locally-preferred types (aka "friends") on a registered type, + * and looking at those friends first; this would be nice in that we could allow private friends + * + * Yoml.read(...) can take a special "top-level type extensions" in order to find friends, + * and/or a special "top-level libraries" in order to resolve types in the first instance. + * These would *not* be passed when resolving a found type. + */ +public class BrooklynYomlTypeRegistry implements YomlTypeRegistry { + + private static final Logger log = LoggerFactory.getLogger(BrooklynYomlTypeRegistry.class); + + private static final String JAVA_PREFIX = "java:"; + + @SuppressWarnings("serial") + static ConfigKey> CACHED_SERIALIZERS = ConfigKeys.newConfigKey(new TypeToken>() {}, "yoml.type.serializers", + "Serializers found for a registered type"); + + private ManagementContext mgmt; + + private RegisteredTypeLoadingContext rootLoadingContext; + + public BrooklynYomlTypeRegistry(@Nonnull ManagementContext mgmt, @Nonnull RegisteredTypeLoadingContext rootLoadingContext) { + this.mgmt = mgmt; + this.rootLoadingContext = rootLoadingContext; + + } + + protected BrooklynTypeRegistry registry() { + return mgmt.getTypeRegistry(); + } + + @Override + public Maybe newInstanceMaybe(String typeName, Yoml yoml) { + return newInstanceMaybe(typeName, yoml, rootLoadingContext); + } + + @Override + public Maybe newInstanceMaybe(String typeName, Yoml yoml, @Nonnull YomlContext yomlContextOfThisEvaluation) { + RegisteredTypeLoadingContext applicableLoadingContext = getTypeContextFor(yomlContextOfThisEvaluation); + return newInstanceMaybe(typeName, yoml, applicableLoadingContext); + } + + protected RegisteredTypeLoadingContext getTypeContextFor(YomlContext yomlContextOfThisEvaluation) { + RegisteredTypeLoadingContext applicableLoadingContext; + if (Strings.isBlank(yomlContextOfThisEvaluation.getJsonPath())) { + // at root, use everything + applicableLoadingContext = rootLoadingContext; + } else { + // elsewhere the only thing we apply is the loader + applicableLoadingContext = RegisteredTypeLoadingContexts.builder().loader(rootLoadingContext.getLoader()).build(); + } + + if (yomlContextOfThisEvaluation.getConstructionInstruction()!=null) { + // also apply any construction instruction we've been given + applicableLoadingContext = RegisteredTypeLoadingContexts.builder(applicableLoadingContext) + .constructorInstruction(yomlContextOfThisEvaluation.getConstructionInstruction()).build(); + } + return applicableLoadingContext; + } + + public Maybe newInstanceMaybe(String typeName, Yoml yoml, @Nonnull RegisteredTypeLoadingContext typeContext) { + // yoml may be null, for java type lookups, but we could potentially get rid of that call path + + RegisteredType typeR = registry().get(typeName, typeContext); + + if (typeR!=null) { + boolean seenType = typeContext.getAlreadyEncounteredTypes().contains(typeName); + + if (!seenType) { + // instantiate the parent type + + // keep everything (supertype constraint, encountered types, constructor instruction) + // apart from loader info -- loader should be from the type found here + RegisteredTypeLoadingContexts.Builder nextContext = RegisteredTypeLoadingContexts.builder(typeContext); + nextContext.addEncounteredTypes(typeName); + // reset the loader (pretty sure this is right -Alex) + // the create call will attach the loader of typeR + nextContext.loader(null); + + return Maybe.of((Object) registry().create(typeR, nextContext.build(), null)); + + } else { + // circular reference means load java, below + } + } else { + // type not found means load java, below + } + + Maybe> t = null; + + Exception e = null; + try { + t = getJavaTypeInternal(typeName, typeContext); + } catch (Exception ee) { + Exceptions.propagateIfFatal(ee); + e = ee; + } + if (t.isAbsent()) { + // generally doesn't come here; it gets filtered by getJavaTypeMaybe + if (e==null) e = ((Maybe.Absent)t).getException(); + return Maybe.absent("Neither the type registry nor the classpath/libraries could load type "+typeName+ + (e!=null && !Exceptions.isRootBoringClassNotFound(e, typeName) ? ": "+Exceptions.collapseText(e) : "")); + } + + try { + return ConstructionInstructions.Factory.newDefault(t.get(), typeContext.getConstructorInstruction()).create(); + } catch (Exception e2) { + return Maybe.absent("Error instantiating type "+typeName+": "+Exceptions.collapseText(e2)); + } + } + + @Override + public Object newInstance(String typeN, Yoml yoml) { + return newInstanceMaybe(typeN, yoml).orNull(); + } + + static class YomlClassNotFoundException extends RuntimeException { + private static final long serialVersionUID = 5946251753146668070L; + public YomlClassNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + /* + * There is also some complexity around the java type and the supertypes. + * For context, the latter is needed so callers can filter appropriately. + * The former is needed so that serializers can filter appropriately + * (the java type is not very extensively used and could be changed to use the supertypes set, + * but we have the same problem for both). + *

+ * The issue is that when adding to the catalog the caller likely supplies + * only the yaml, not a statement of supertypes. So we have to try to figure out + * the supertypes. We can do this (1) on-load, when it is added, or (2) lazy, when it is accessed. + * We're going to do (1), and persisting the results, which means we instantiate + * each item once on addition: this serves as a useful validation, but it does disallow + * forward references (ie referenced types must be declared first; for "templates" we could skip this), + * but we avoid re-instantiation on rebind (and the ordering issues that can arise there). + *

+ * Option (2) while it has some attractions it makes the API more entangled between RegisteredType and the transformer, + * and risks odd errors depending when the lazy evaluation occurs vis-a-vis dependent types. + */ + @Override + public Maybe> getJavaTypeMaybe(String typeName, YomlContext context) { + if (typeName==null) return Maybe.absent("null type"); + return getJavaTypeInternal(typeName, getTypeContextFor(context)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected static Maybe> maybeClass(Class clazz) { + // restrict unchecked wildcard generic warning/suppression to here + return (Maybe) Maybe.of(clazz); + } + + protected Maybe> getJavaTypeInternal(String typeName, RegisteredTypeLoadingContext context) { + RegisteredType type = registry().get(typeName, context); + if (type!=null && context!=null && !context.getAlreadyEncounteredTypes().contains(type.getId())) { + return getJavaTypeInternal(type, context); + } + + // try to load it wrt context + Class result = null; + + // strip generics for the purposes here + if (typeName.indexOf('<')>0) typeName = typeName.substring(0, typeName.indexOf('<')); + + if (result==null) result = Boxing.boxedType(Boxing.getPrimitiveType(typeName).orNull()); + if (result==null && YomlUtils.TYPE_STRING.equals(typeName)) result = String.class; + + if (result!=null) return maybeClass(result); + + // currently we accept but don't require the 'java:' prefix + boolean isJava = typeName.startsWith(JAVA_PREFIX); + typeName = Strings.removeFromStart(typeName, JAVA_PREFIX); + BrooklynClassLoadingContext loader = null; + if (context!=null && context.getLoader()!=null) + loader = context.getLoader(); + else + loader = JavaBrooklynClassLoadingContext.create(mgmt); + + Maybe> resultM = loader.tryLoadClass(typeName); + if (resultM.isPresent()) return resultM; + if (isJava) return resultM; // if java was explicit, give the java load error + RuntimeException e = ((Maybe.Absent)resultM).getException(); + return Maybe.absent("Neither the type registry nor the classpath/libraries could find type "+typeName+ + (e!=null && !Exceptions.isRootBoringClassNotFound(e, typeName) ? ": "+Exceptions.collapseText(e) : "")); + } + + protected Maybe> getJavaTypeInternal(RegisteredType type, RegisteredTypeLoadingContext context) { + { + Class result = RegisteredTypes.peekActualJavaType(type); + if (result!=null) return maybeClass(result); + } + + String declaredPrimarySuperTypeName = null; + + if (type.getPlan() instanceof YomlTypeImplementationPlan) { + declaredPrimarySuperTypeName = ((YomlTypeImplementationPlan)type.getPlan()).javaType; + } + if (declaredPrimarySuperTypeName==null) { + log.warn("Primary java super type not declared for "+type+"; it should have been specified/inferred at definition time; will try to infer it now"); + // first look at plan, for a `type` block + if (type.getPlan() instanceof YomlTypeImplementationPlan) { + Maybe> map = RegisteredTypes.getAsYamlMap(type.getPlan().getPlanData()); + if (map.isPresent()) declaredPrimarySuperTypeName = Strings.toString(map.get().get("type")); + } + } + if (declaredPrimarySuperTypeName==null) { + // could look at declared super types, instantiate, and take the lowest common if found + // (but the above is recommended and the preloading below sufficient) + } + + Maybe> result = Maybe.absent("Unable to find java supertype for "+type); + + if (declaredPrimarySuperTypeName!=null) { + // when looking at supertypes we reset the loader to use loaders for the type, + // note the traversal in encountered types, and leave the rest (eg supertype restriction) as was + RegisteredTypeLoadingContext newContext = RegisteredTypeLoadingContexts.builder(context) + .loader( CatalogUtils.newClassLoadingContext(mgmt, type) ) + .addEncounteredTypes(type.getId()) + .build(); + + result = getJavaTypeInternal(declaredPrimarySuperTypeName, newContext); + } + + if (result.isAbsent()) { + RegisteredTypeLoadingContext newContext = RegisteredTypeLoadingContexts.alreadyEncountered(context.getAlreadyEncounteredTypes()); + // failing that, instantiate it (caching it as type object to prevent recursive lookups?) + log.warn("Preloading required to determine type for "+type); + Maybe m = newInstanceMaybe(type.getId(), null, newContext); + log.info("Preloading completed, got "+m+" for "+type); + if (m.isPresent()) result = maybeClass(m.get().getClass()); + else { + // if declared java type, prefer that error, otherwise give error from above + if (declaredPrimarySuperTypeName==null) { + result = Maybe.absent(new IllegalStateException("Unable to find java supertype declared on "+type+" and unable to instantiate", + ((Maybe.Absent)result).getException())); + } + } + } + + if (result.isPresent()) { + RegisteredTypes.cacheActualJavaType(type, result.get()); + } + return result; + } + + @Override + public String getTypeName(Object obj) { + return getTypeNameOfClass(obj.getClass()); + } + + public static Set WARNS = MutableSet.of( + // don't warn on this base class + JAVA_PREFIX+Object.class.getName() ); + + @Override + public String getTypeNameOfClass(Class type) { + if (type==null) return null; + + String defaultTypeName = getDefaultTypeNameOfClass(type); + String cleanedTypeName = Strings.removeFromStart(getDefaultTypeNameOfClass(type), JAVA_PREFIX); + + // the code below may be a bottleneck; if so, we should cache or something more efficient + + Set types = MutableSet.of(); + // look in catalog for something where plan matches and consists only of type + for (RegisteredType rt: mgmt.getTypeRegistry().getAll()) { + if (!(rt.getPlan() instanceof YomlTypeImplementationPlan)) continue; + if (((YomlTypeImplementationPlan)rt.getPlan()).javaType==null) continue; + if (!((YomlTypeImplementationPlan)rt.getPlan()).javaType.equals(cleanedTypeName)) continue; + if (rt.getPlan().getPlanData()==null) types.add(rt); + // are there ever plans we want to permit, eg just defining serializers? + // (if so check the plan here) + } + if (types.size()==1) return Iterables.getOnlyElement(types).getSymbolicName(); + if (types.size()>1) { + if (WARNS.add(type.getName())) + log.warn("Multiple registered types for "+type+"; picking one arbitrarily"); + return types.iterator().next().getId(); + } + + boolean isJava = defaultTypeName.startsWith(JAVA_PREFIX); + if (isJava && WARNS.add(type.getName())) + log.warn("Returning default for type name of "+type+"; catalog entry should be supplied"); + + return defaultTypeName; + } + + protected String getDefaultTypeNameOfClass(Class type) { + Maybe primitive = Boxing.getPrimitiveName(type); + if (primitive.isPresent()) return primitive.get(); + if (String.class.equals(type)) return "string"; + // map and list handled by those serializers + return JAVA_PREFIX+type.getName(); + } + + @Override + public Iterable getSerializersForType(String typeName, YomlContext yomlContext) { + Set result = MutableSet.of(); + // TODO add root loader? + collectSerializers(typeName, getTypeContextFor(yomlContext), result, MutableSet.of()); + return result; + } + + protected void collectSerializers(Object type, RegisteredTypeLoadingContext context, Collection result, Set typesVisited) { + if (type==null) return; + if (type instanceof String) { + // convert string to registered type or class + Object typeR = registry().get((String)type); + if (typeR==null) { + typeR = getJavaTypeInternal((String)type, context).orNull(); + } + if (typeR==null) { + // will this ever happen in normal operations? + log.warn("Could not find '"+type+" when collecting serializers"); + return; + } + type = typeR; + } + boolean canUpdateCache = typesVisited.isEmpty(); + if (!typesVisited.add(type)) return; // already seen + Set supers = MutableSet.of(); + if (type instanceof RegisteredType) { + List serializers = ((BasicRegisteredType)type).getCache().get(CACHED_SERIALIZERS); + if (serializers!=null) { + canUpdateCache = false; + result.addAll(serializers); + } else { + // TODO don't cache if it's a snapshot version + // (also should invalidate any subtypes, but actually any subtypes of snapshots + // should also be snapshots -- that should be enforced!) +// if (isSnapshot( ((RegisteredType)type).getVersion() )) updateCache = false; + + // apply serializers from this RT + TypeImplementationPlan plan = ((RegisteredType)type).getPlan(); + if (plan instanceof YomlTypeImplementationPlan) { + result.addAll( ((YomlTypeImplementationPlan)plan).serializers ); + } + + // loop over supertypes for serializers declared there (unless we introduce an option to suppress/limit) + supers.addAll(((RegisteredType) type).getSuperTypes()); + } + } else if (type instanceof Class) { + result.addAll(new BrooklynYomlAnnotations().findSerializerAnnotations((Class)type, false)); +// // could look up the type? but we should be calling this normally with the RT if we have one so probably not necessary +// // and could recurse through superclasses and interfaces -- but the above is a better place to do that if needed +// String name = getTypeNameOfClass((Class)type); +// if (name.startsWith(JAVA_PREFIX)) { +// find... +//// supers.add(((Class) type).getSuperclass()); +//// supers.addAll(Arrays.asList(((Class) type).getInterfaces())); +// } + } else { + throw new IllegalStateException("Illegal supertype entry "+type+", visiting "+typesVisited); + } + for (Object s: supers) { + RegisteredTypeLoadingContext unconstrainedSupertypeContext = RegisteredTypeLoadingContexts.builder(context) + .expectedSuperType(null).build(); + collectSerializers(s, unconstrainedSupertypeContext, result, typesVisited); + } + if (canUpdateCache) { + if (type instanceof RegisteredType) { + ((BasicRegisteredType)type).getCache().put(CACHED_SERIALIZERS, ImmutableList.copyOf(result)); + } else { + // could use static weak cache map on classes? if so also update above + } + } + } + + public static RegisteredType newYomlRegisteredType(RegisteredTypeKind kind, + String symbolicName, String version, String planData, + Class javaConcreteType, + Iterable addlSuperTypesAsClassOrRegisteredType, + Iterable serializers) { + + YomlTypeImplementationPlan plan = new YomlTypeImplementationPlan(planData, javaConcreteType, serializers); + RegisteredType result = kind==RegisteredTypeKind.SPEC ? RegisteredTypes.spec(symbolicName, version, plan) : RegisteredTypes.bean(symbolicName, version, plan); + RegisteredTypes.addSuperType(result, javaConcreteType); + RegisteredTypes.addSuperTypes(result, addlSuperTypesAsClassOrRegisteredType); + return result; + } + + /** null symbolic name means to take it from annotations or the class name */ + public static RegisteredType newYomlRegisteredType(RegisteredTypeKind kind, @Nullable String symbolicName, String version, Class clazz) { + Set names = new BrooklynYomlAnnotations().findTypeNamesFromAnnotations(clazz, symbolicName, false); + + Set serializers = new BrooklynYomlAnnotations().findSerializerAnnotations(clazz, false); + + RegisteredType type = BrooklynYomlTypeRegistry.newYomlRegisteredType(kind, + // symbolicName, version, + names.iterator().next(), version, + // planData - null means just use the java type (could have done this earlier), + null, + // javaConcreteType, superTypesAsClassOrRegisteredType, serializers) + clazz, Arrays.asList(clazz), serializers); + type = RegisteredTypes.addAliases(type, names); + return type; + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/YomlTypePlanTransformer.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/YomlTypePlanTransformer.java new file mode 100644 index 0000000000..0e4ee33119 --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/YomlTypePlanTransformer.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslInterpreter; +import org.apache.brooklyn.camp.spi.resolve.PlanInterpreter; +import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationContext; +import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode; +import org.apache.brooklyn.core.catalog.internal.CatalogUtils; +import org.apache.brooklyn.core.typereg.AbstractFormatSpecificTypeImplementationPlan; +import org.apache.brooklyn.core.typereg.AbstractTypePlanTransformer; +import org.apache.brooklyn.core.typereg.RegisteredTypeInfo; +import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; +import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.core.typereg.UnsupportedTypePlanException; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.core.task.ValueResolver; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions; +import org.apache.brooklyn.util.yoml.internal.YomlConfigs.Builder; +import org.yaml.snakeyaml.Yaml; + +import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +/** + * Makes it possible for Brooklyn to resolve YOML items, + * both types registered with the system using YOML + * and plans in the YOML format (sent here as unregistered types), + * and supporting any objects and special handling for "spec" objects. + * + * DONE + * - test programmatic addition and parse (beans) with manual objects + * - figure out supertypes and use that to determine java type + * - attach custom serializers (on plan) + * - test serializers + * - support serializers by annotation + * - set serializers when adding to catalog and test + * - $brooklyn:object-yoml: + * - support $brooklyn DSL in yoml + * + * NEXT + * - catalog impl supports format + * + * (initdish can now be made to work) + * + * THEN + * - update docs for above, including $brooklyn:object-yoml, yoml overview + * - support specs from yoml + * - type registry api, add arbitrary types via yoml, specifying format + * - catalog impl in yoml as test? + * - type registry persistence + * - REST API for deploy accepts specific format, can call yoml (can we test this earlier?) + * + * AND THEN + * - generate its own documentation + * - persistence switches to using yoml, warns if any types are not yoml-ized + * - yoml allows `constructor: [list]` and `constructor: { mode: static, type: factory, method: newInstance, args: ... }` + * and maybe even `constructor: { mode: chain, steps: [ { mode: constructor, type: Foo.Builder }, { mode: method, method: bar, args: [ true ] }, { mode: method, method: build } ] }` + * - type access control -- ie restrict who can see what types + * - java instantiation access control - ie permission required to access java in custom types (deployed or added to catalog) + */ +public class YomlTypePlanTransformer extends AbstractTypePlanTransformer { + + private static final List FORMATS = ImmutableList.of("yoml"); + + public static final String FORMAT = FORMATS.get(0); + + public YomlTypePlanTransformer() { + super(FORMAT, "YOML Brooklyn syntax", "Standard YOML adapters for Apache Brooklyn including OASIS CAMP"); + } + + private final static Set IGNORE_SINGLE_KEYS = ImmutableSet.of("name", "version"); + + @Override + protected double scoreForNullFormat(Object planData, RegisteredType type, RegisteredTypeLoadingContext context) { + int score = 0; + Maybe> plan = RegisteredTypes.getAsYamlMap(planData); + if (plan.isPresent()) { + if (plan.get().size()>1 || (plan.get().size()==1 && !IGNORE_SINGLE_KEYS.contains(plan.get().keySet().iterator().next()))) { + // weed out obvious bad plans -- in part so that tests pass + // TODO in future we should give a tiny score to anything else (once we want to enable this as a popular auto-detetction target) +// score += 1; + // but for now we require at least one recognised keyword + } + if (plan.get().containsKey("type")) score += 5; + if (plan.get().containsKey("services")) score += 2; + } + if (type instanceof YomlTypeImplementationPlan) score += 100; + return (1.0 - 8.0/(score+8)); + } + + @Override + protected double scoreForNonmatchingNonnullFormat(String planFormat, Object planData, RegisteredType type, RegisteredTypeLoadingContext context) { + if (FORMATS.contains(planFormat.toLowerCase())) return 0.9; + return 0; + } + + @Override + protected AbstractBrooklynObjectSpec createSpec(RegisteredType type, RegisteredTypeLoadingContext context) throws Exception { + // TODO + throw new UnsupportedTypePlanException("YOML doesn't yet support specs"); + } + + @Override + protected Object createBean(RegisteredType type, RegisteredTypeLoadingContext context) throws Exception { + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(context); + + // add any loaders for this type + context = RegisteredTypeLoadingContexts.builder(context) + .loader( + CatalogUtils.newClassLoadingContext(mgmt, type, context.getLoader()) ) + .build(); + + Yoml y = Yoml.newInstance(newYomlConfig(mgmt, context).build()); + + // TODO could cache the parse, could cache the instantiation instructions + Object data = type.getPlan().getPlanData(); + + Class expectedSuperType = context.getExpectedJavaSuperType(); + String expectedSuperTypeName = y.getConfig().getTypeRegistry().getTypeNameOfClass(expectedSuperType); + + Object parsedInput; + if (data==null || (data instanceof String)) { + if (Strings.isBlank((String)data)) { + // blank plan means to use the java type / construction instruction + Maybe> javaType = ((BrooklynYomlTypeRegistry) y.getConfig().getTypeRegistry()).getJavaTypeInternal(type, context); + ConstructionInstruction constructor = context.getConstructorInstruction(); + if (javaType.isAbsent() && constructor==null) { + return Maybe.absent(new IllegalStateException("Type "+type+" has no plan YAML and error in type", ((Maybe.Absent)javaType).getException())); + } + + Maybe result = ConstructionInstructions.Factory.newDefault(javaType.get(), constructor).create(); + + if (result.isAbsent()) { + throw new YomlException("Type '"+type+"' has no plan and its java type cannot be instantiated", ((Maybe.Absent)result).getException()); + } + return result.get(); + } + + // else we need to parse to json objects, then translate it (below) + parsedInput = new Yaml().load((String) data); + + } else { + // we do this (supporint non-string and pre-parsed plans) in order to support the YAML DSL + // and other limited use cases (see DslYomlObject); + // NB this is only for ad hoc plans (code above) and we may deprecate it altogether as soon as we can + // get the underlying string in the $brooklyn DSL context + + if (type.getSymbolicName()!=null) { + throw new IllegalArgumentException("The implementation plan for '"+type+"' should be a string in order to process as YOML"); + } + parsedInput = data; + + } + + // in either case, now run interpreters (dsl) if it's a m=ap, then translate + + if (parsedInput instanceof Map) { + List interpreters = MutableList.of(new BrooklynDslInterpreter()); + @SuppressWarnings("unchecked") + PlanInterpretationNode interpretation = new PlanInterpretationNode( + new PlanInterpretationContext((Map)parsedInput, interpreters)); + parsedInput = interpretation.getNewValue(); + } + + return y.readFromYamlObject(parsedInput, expectedSuperTypeName); + } + + @Beta @VisibleForTesting + public static Builder newYomlConfig(@Nonnull ManagementContext mgmt, @Nullable RegisteredTypeLoadingContext context) { + if (context==null) context = RegisteredTypeLoadingContexts.any(); + BrooklynYomlTypeRegistry tr = new BrooklynYomlTypeRegistry(mgmt, context); + return YomlConfig.Builder.builder().typeRegistry(tr). + serializersPostAddDefaults(). + // could add any custom global serializers here; but so far these are all linked to types + // and collected by tr.collectSerializers(...) + coercer(new ValueResolver.ResolvingTypeCoercer()); + } + + @Override + public RegisteredTypeInfo getTypeInfo(RegisteredType type) { + // TODO + return RegisteredTypeInfo.create(type, this, null, MutableSet.of()); + } + + static class YomlTypeImplementationPlan extends AbstractFormatSpecificTypeImplementationPlan { + final String javaType; + final List serializers; + + public YomlTypeImplementationPlan(String planData, Class javaType, Iterable serializers) { + super(FORMATS.get(0), planData); + this.javaType = Preconditions.checkNotNull(javaType).getName(); + this.serializers = MutableList.copyOf(serializers); + } + } +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigBagConstructionWithArgsInstruction.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigBagConstructionWithArgsInstruction.java new file mode 100644 index 0000000000..15f82b8a1d --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigBagConstructionWithArgsInstruction.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.serializers; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions.WrappingConstructionInstruction; + +import com.google.common.collect.Iterables; + +/** Like {@link ConfigKeyMapConstructionWithArgsInstruction} but passing a {@link ConfigBag} as the single argument + * to the constructor. */ +public class ConfigBagConstructionWithArgsInstruction extends ConfigKeyMapConstructionWithArgsInstruction { + + public ConfigBagConstructionWithArgsInstruction(Class type, Map values, + WrappingConstructionInstruction optionalOuter, Map> keysByAlias) { + super(type, values, optionalOuter, keysByAlias); + } + + @Override + protected List combineArguments(List constructorsSoFarOutermostFirst) { + return MutableList.of( + ConfigBag.newInstance( + (Map)Iterables.getOnlyElement( super.combineArguments(constructorsSoFarOutermostFirst) ))); + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyConstructionInstructions.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyConstructionInstructions.java new file mode 100644 index 0000000000..8800fdaa1c --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyConstructionInstructions.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions.WrappingConstructionInstruction; + +public class ConfigKeyConstructionInstructions { + + /** As the others, but expecting a one-arg constructor which takes a config map; + * this will deduce the appropriate values by looking at + * the inheritance declarations at the keys and at the values at the outer and inner constructor instructions. */ + public static ConstructionInstruction newUsingConfigKeyMapConstructor(Class type, + Map values, + ConstructionInstruction optionalOuter, + Map> keysByAlias) { + return new ConfigKeyMapConstructionWithArgsInstruction(type, values, (WrappingConstructionInstruction) optionalOuter, + keysByAlias); + } + + public static ConstructionInstruction newUsingConfigBagConstructor(Class type, + Map values, + ConstructionInstruction optionalOuter, + Map> keysByAlias) { + return new ConfigBagConstructionWithArgsInstruction(type, values, (WrappingConstructionInstruction) optionalOuter, + keysByAlias); + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyMapConstructionWithArgsInstruction.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyMapConstructionWithArgsInstruction.java new file mode 100644 index 0000000000..aebae24fcf --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/ConfigKeyMapConstructionWithArgsInstruction.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.serializers; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.config.ConfigInheritance; +import org.apache.brooklyn.config.ConfigInheritances; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.config.ConfigValueAtContainer; +import org.apache.brooklyn.core.config.BasicConfigInheritance; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.config.ConfigKeys.InheritanceContext; +import org.apache.brooklyn.core.config.internal.AncestorContainerAndKeyValueIterator; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions.AbstractConstructionWithArgsInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions.WrappingConstructionInstruction; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.collect.Iterables; + +/** Instruction for using a constructor which takes a single argument being a map of string,object + * config keys. This will look at the actual keys defined on the types and traverse the construction + * instruction hierarchy to ensure the key values are inherited correctly from supertype definitions. */ +public class ConfigKeyMapConstructionWithArgsInstruction extends AbstractConstructionWithArgsInstruction { + protected final Map> keysByNameOrAlias; + + public ConfigKeyMapConstructionWithArgsInstruction(Class type, + Map values, + WrappingConstructionInstruction optionalOuter, + Map> keysByNameOrAlias) { + super(type, MutableList.of(values), optionalOuter); + this.keysByNameOrAlias = keysByNameOrAlias; + } + + @Override + protected List combineArguments(final List constructorsSoFarOutermostFirst) { + Set innerNames = MutableSet.of(); + for (ConstructionInstruction i: constructorsSoFarOutermostFirst) { + innerNames.addAll(getKeyValuesAt(i).keySet()); + } + + // collect all keys, real local ones, and anonymous ones for anonymous config from parents + + // TODO if keys are defined at yaml-based type or type parent we don't currently have a way to get them + // (the TypeRegistry API needs to return more than just java class for that) + final Map> keysByName = MutableMap.of(); + + for (ConfigKey keyDeclaredHere: keysByNameOrAlias.values()) { + keysByName.put(keyDeclaredHere.getName(), keyDeclaredHere); + } + + MutableMap,Object> localValues = MutableMap.of(); + for (Map.Entry aliasAndValueHere: getKeyValuesAt(this).entrySet()) { + ConfigKey k = keysByNameOrAlias.get(aliasAndValueHere.getKey()); + if (k==null) { + // don't think it should come here; all keys will be known + k = anonymousKey(aliasAndValueHere.getKey()); + } + if (!keysByName.containsKey(k.getName())) { + keysByName.put(k.getName(), k); + } + if (!localValues.containsKey(k.getName())) { + localValues.put(k, aliasAndValueHere.getValue()); + } + } + + for (String innerKeyName: innerNames) { + if (!keysByName.containsKey(innerKeyName)) { + // parent defined a value under a key which doesn't match config keys we know + keysByName.put(innerKeyName, anonymousKey(innerKeyName)); + } + } + + Map result = MutableMap.of(); + for (final ConfigKey k: keysByName.values()) { + // go through all keys defined here recognising aliases, + // and anonymous keys for other keys at parents (ignoring aliases) + Maybe value = localValues.containsKey(k) ? Maybe.ofAllowingNull(localValues.get(k)) : Maybe.absent(); + // don't set default values +// Maybe defaultValue = k.hasDefaultValue() ? Maybe.ofAllowingNull((Object)k.getDefaultValue()) : Maybe.absent(); + + Function> keyFn = new Function>() { + @SuppressWarnings("unchecked") + @Override + public ConfigKey apply(ConstructionInstruction input) { + // type inheritance so pretty safe to assume outermost key declaration + return (ConfigKey) keysByName.get(input); + } + }; + Function> lookupFn = new Function>() { + @Override + public Maybe apply(ConstructionInstruction input) { + Map values = getKeyValuesAt(input); // TODO allow aliases? + if (values.containsKey(k.getName())) return Maybe.of((Object)values.get(k.getName())); + return Maybe.absent(); + } + }; + Function, Maybe> coerceFn = Functions.identity(); + Function parentFn = new Function() { + @Override + public ConstructionInstruction apply(ConstructionInstruction input) { + // parent is the one *after* us in the list, *not* input.getOuterInstruction() + Iterator ci = constructorsSoFarOutermostFirst.iterator(); + ConstructionInstruction cc = ConfigKeyMapConstructionWithArgsInstruction.this; + while (ci.hasNext()) { + if (input.equals(cc)) { + return ci.next(); + } + cc = ci.next(); + } + return null; + } + }; + Iterator> ancestors = new AncestorContainerAndKeyValueIterator( + this, keyFn, lookupFn, coerceFn, parentFn); + + ConfigInheritance inheritance = ConfigInheritances.findInheritance(k, InheritanceContext.TYPE_DEFINITION, BasicConfigInheritance.OVERWRITE); + + @SuppressWarnings("unchecked") + ReferenceWithError> newValue = + ConfigInheritances.resolveInheriting(this, (ConfigKey)k, value, Maybe.absent(), + ancestors, InheritanceContext.TYPE_DEFINITION, inheritance); + + if (newValue.getWithError().isValueExplicitlySet()) + result.put(k.getName(), newValue.getWithError().get()); + } + + return MutableList.of(result); + } + + protected ConfigKey anonymousKey(String key) { + return ConfigKeys.newConfigKey(Object.class, key); + } + + @SuppressWarnings("unchecked") + protected Map getKeyValuesAt(ConstructionInstruction i) { + if (i.getArgs()==null) return MutableMap.of(); + if (i.getArgs().size()!=1) throw new IllegalArgumentException("Wrong length of constructor params, expected one: "+i.getArgs()); + Object arg = Iterables.getOnlyElement(i.getArgs()); + if (arg==null) return MutableMap.of(); + if (!(arg instanceof Map)) throw new IllegalArgumentException("Wrong type of constructor param, expected map: "+arg); + return (Map)arg; + } +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigBag.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigBag.java new file mode 100644 index 0000000000..36ff98c3ec --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigBag.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.serializers; + +import java.lang.reflect.Field; +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistryUsingConfigMap; + +@Alias("config-bag-constructor") +public class InstantiateTypeFromRegistryUsingConfigBag extends InstantiateTypeFromRegistryUsingConfigMap { + + public static class Factory extends InstantiateTypeFromRegistryUsingConfigMap.Factory { + protected InstantiateTypeFromRegistryUsingConfigBag newInstance() { + return new InstantiateTypeFromRegistryUsingConfigBag(); + } + } + + protected Maybe findConstructorMaybe(Class type) { + Maybe c = findConfigBagConstructor(type); + if (c.isPresent()) return c; + Maybe c2 = super.findConstructorMaybe(type); + if (c2.isPresent()) return c2; + + return c; + } + protected Maybe findConfigBagConstructor(Class type) { + return Reflections.findConstructorExactMaybe(type, ConfigBag.class); + } + + protected Maybe findFieldMaybe(Class type) { + Maybe f = Reflections.findFieldMaybe(type, fieldNameForConfigInJava); + if (f.isPresent() && !(Map.class.isAssignableFrom(f.get().getType()) || ConfigBag.class.isAssignableFrom(f.get().getType()))) + f = Maybe.absent(); + return f; + } + + @Override + protected Map getRawConfigMap(Field f, Object obj) throws IllegalAccessException { + if (ConfigBag.class.isAssignableFrom(f.getType())) { + return ((ConfigBag)f.get(obj)).getAllConfig(); + } + return super.getRawConfigMap(f, obj); + } + + @Override + protected ConstructionInstruction newConstructor(Class type, Map> keysByAlias, + Map fieldsFromReadToConstructJava, ConstructionInstruction optionalOuter) { + if (findConfigBagConstructor(type).isPresent()) { + return ConfigKeyConstructionInstructions.newUsingConfigBagConstructor(type, fieldsFromReadToConstructJava, optionalOuter, + keysByAlias); + } + return ConfigKeyConstructionInstructions.newUsingConfigKeyMapConstructor(type, fieldsFromReadToConstructJava, optionalOuter, + keysByAlias); + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigKeyMap.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigKeyMap.java new file mode 100644 index 0000000000..7750714e33 --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/serializers/InstantiateTypeFromRegistryUsingConfigKeyMap.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistryUsingConfigMap; + +@Alias("config-map-constructor") +public class InstantiateTypeFromRegistryUsingConfigKeyMap extends InstantiateTypeFromRegistryUsingConfigMap { + + public static class Factory extends InstantiateTypeFromRegistryUsingConfigMap.Factory { + protected InstantiateTypeFromRegistryUsingConfigKeyMap newInstance() { + return new InstantiateTypeFromRegistryUsingConfigKeyMap(); + } + } + + @Override + protected ConstructionInstruction newConstructor(Class type, Map> keysByAlias, + Map fieldsFromReadToConstructJava, ConstructionInstruction optionalOuter) { + return ConfigKeyConstructionInstructions.newUsingConfigKeyMapConstructor(type, fieldsFromReadToConstructJava, optionalOuter, + keysByAlias); + } + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/types/YomlInitializers.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/types/YomlInitializers.java new file mode 100644 index 0000000000..1826ec236a --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/yoml/types/YomlInitializers.java @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.types; + +import org.apache.brooklyn.api.entity.EntityInitializer; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.camp.yoml.BrooklynYomlTypeRegistry; +import org.apache.brooklyn.core.BrooklynVersion; +import org.apache.brooklyn.core.effector.ssh.SshCommandEffector; +import org.apache.brooklyn.core.sensor.StaticSensor; +import org.apache.brooklyn.core.sensor.ssh.SshCommandSensor; +import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.yoml.YomlSerializer; + +import com.google.common.annotations.Beta; + +/* + +TYPES - effector +- script / bash / ssh +- invoke-effector +- publish-sensor +- initdish-effector +- parallel +- sequence + + - ssh + - install-file + - set-sensor + - set-config + + +SEQ +* initd +* specify pre-conditions & post-conditions +* specify what must run before/after + +a +b after a +c before b + +EXTENSIONS: +* local task parameters -- could pass params to any subtask +* acquire-semaphore (cancel holder, timeout + +* parallel +* run over entities -- concurrency: 16 +* conditional +* jumping? +* set sensors/config +* access results of previous tasks + + +STYLE NOTES + + brooklyn.initializers: + - type: org.apache.brooklyn.core.effector.ssh.SshCommandEffector + brooklyn.config: + name: sayHiNetcat + description: Echo a small hello string to the netcat entity + command: | + echo $message | nc $TARGET_HOSTNAME 4321 + parameters: + message: + description: The string to pass to netcat + defaultValue: hi netcat + +effectors: + say-hi: + type: script + env: + name: $brooklyn:config("name") + name: $brooklyn:${name} + script: + echo hello ${name:-world} + + publish-name: + type: publish-sensor + sensor: name + value: $brooklyn:formatString("%s (%s)", config("name"), $("baz")) + parameters: + foo: # nothing + bar: { description: The bar, default-value: B } + baz: Z # default + + start: + type: initdish-effector + impl: + 8.2-something: + type: invoke-effector + effector: say-hi + parameters: + name: Bob + 8.2-something: + invoke-effector: say-hi + 8.2-something: + invoke-effector: + effector: say-hi + parameters: + name: Bob + 8.2-something: + "saying hi": + type: invoke-effector + effector: say-hi + parameters: + name: Bob + + +COMPARISON TO OLD + +we used to say: + + brooklyn.initializers: + - type: org.apache.brooklyn.core.effector.ssh.SshCommandEffector + brooklyn.config: + name: sayHiNetcat + description: Echo a small hello string to the netcat entity + command: | + echo $message | nc $TARGET_HOSTNAME 4321 + parameters: + message: + description: The string to pass to netcat + defaultValue: hi netcat + - type: org.apache.brooklyn.core.sensor.ssh.SshCommandSensor + brooklyn.config: + name: output.last + period: 1s + command: tail -1 server-input + +now we say: + + brooklyn.initializers: + say-hi-netcat: + type: ssh-effector + description: Echo a small hello string to the netcat entity + script: | + echo $message | nc $TARGET_HOSTNAME 4321 + parameters: + message: + description: The string to pass to netcat + default-value: hi netcat + output.last: + type: ssh-sensor + period: 1s + command: tail -1 server-input + +benefits: + - readable: more concise description and supports aliases, maps (which plays nice with merge) + - extensible: *much* easier to define new items, including recursive + - introspective: auto-generate docs and support code completion with descriptions + - bi-directional: we can persist in the same formal + + + +OTHER THOUGHTS + +effectors: + - type: CommandSequenceEffector + name: start + impl: + - sequence_identifier: 0-provision [optional] + type: my.ProvisionEffectorTaskFactory + - seq_id: 1-install + type: script + script: | + echo foo ${entity.config.foo} + + + 01-install: + parallel: + - bash: | + echo foo ${entity.config.foo} + - copy: + from: URL + to: ${entity.sensor['run.dir']}/file.txt + 02-loop-entities: + foreach: + expression: $brooklyn:component['x'].descendants + var: x + command: + effector: + name: restart + parameters: + restart_machine: auto + 03-condition: + conditional: + if: $x + then: + bash: echo yup + else: + - if: $y + then: + bash: echo y + - bash: echo none + 04-jump: + goto: 02-install + + + + + +catalog: + id: my-basic-1 +services: +- type: vanilla-software-process + effectors: + start: + 0-provision: get-machine + 1-install: + - copy: + from: classpath://foo.tar + to: /tmp/foo/foo.tar + - bash: + - cd /tmp/foo + - unzip foo.tar + - bash: foo.sh + 2-launch: + - cd /tmp/foo ; ./foo.sh + policy: + - type: Hook1 + + triggers: + - on: $brooklyn:component("db").sensor("database_url") + do: + - bash: + mysql install table + - publish_sensor basic-ready true + +---- + +services: +- type: my-basic-2 + effectors: + start: + 1.5.11-post-install: + bash: | +echo custom step + + 1.7-another-post-install: + + */ +public class YomlInitializers { + + /** + * Adds the given type to the registry. + * As the registry is not yet persisted this method must be called explicitly to initialize any management context using it. + * This method will be deleted when there is a catalog-style init/persistence mechanism. */ + @Beta + public static void addLocalType(ManagementContext mgmt, RegisteredType type) { + ((BasicBrooklynTypeRegistry) mgmt.getTypeRegistry()).addToLocalUnpersistedTypeRegistry(type, false); + } + + /** As {@link #addLocalType(ManagementContext, RegisteredType)} */ @Beta + public static void addLocalBean(ManagementContext mgmt, Class clazz) { + addLocalType(mgmt, BrooklynYomlTypeRegistry.newYomlRegisteredType(RegisteredTypeKind.BEAN, null, BrooklynVersion.get(), clazz)); + } + + /** As {@link #addLocalType(ManagementContext, RegisteredType)} */ @Beta + public static void addLocalBean(ManagementContext mgmt, String symbolicName, String planYaml, + Class javaConcreteType, + Iterable addlSuperTypesAsClassOrRegisteredType, + Iterable serializers) { + addLocalType(mgmt, BrooklynYomlTypeRegistry.newYomlRegisteredType(RegisteredTypeKind.BEAN, null, BrooklynVersion.get(), + planYaml, javaConcreteType, addlSuperTypesAsClassOrRegisteredType, serializers)); + } + + /** Put here until there is a better init mechanism */ + @Beta + public static void install(ManagementContext mgmt) { + + addLocalBean(mgmt, Duration.class); + + addLocalBean(mgmt, EntityInitializer.class); + addLocalBean(mgmt, StaticSensor.class); + addLocalBean(mgmt, SshCommandSensor.class); + addLocalBean(mgmt, SshCommandEffector.class); + } + +} diff --git a/camp/camp-brooklyn/src/main/resources/META-INF/services/org.apache.brooklyn.core.typereg.BrooklynTypePlanTransformer b/camp/camp-brooklyn/src/main/resources/META-INF/services/org.apache.brooklyn.core.typereg.BrooklynTypePlanTransformer index 0c6fab3321..122e117c61 100644 --- a/camp/camp-brooklyn/src/main/resources/META-INF/services/org.apache.brooklyn.core.typereg.BrooklynTypePlanTransformer +++ b/camp/camp-brooklyn/src/main/resources/META-INF/services/org.apache.brooklyn.core.typereg.BrooklynTypePlanTransformer @@ -16,4 +16,5 @@ # specific language governing permissions and limitations # under the License. # +org.apache.brooklyn.camp.yoml.YomlTypePlanTransformer org.apache.brooklyn.camp.brooklyn.spi.creation.CampTypePlanTransformer diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpRequestSensorYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpRequestSensorYamlTest.java index f50f27f271..8dc9333f21 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpRequestSensorYamlTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpRequestSensorYamlTest.java @@ -42,7 +42,7 @@ public class HttpRequestSensorYamlTest extends AbstractYamlRebindTest { private static final Logger log = LoggerFactory.getLogger(HttpRequestSensorYamlTest.class); final static AttributeSensor SENSOR_STRING = Sensors.newStringSensor("aString"); - final static String TARGET_TYPE = "java.lang.String"; + final static String TARGET_TYPE = String.class.getName(); private TestHttpServer server; private String serverUrl; diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynDslInYomlStringPlanTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynDslInYomlStringPlanTest.java new file mode 100644 index 0000000000..c4f7fc3080 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynDslInYomlStringPlanTest.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.camp.yoml.types.YomlInitializers; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.entity.EntityInternal; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.time.Time; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.base.Supplier; +import com.google.common.collect.Iterables; + +public class BrooklynDslInYomlStringPlanTest extends AbstractYamlTest { + + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(BrooklynDslInYomlStringPlanTest.class); + + private BasicBrooklynTypeRegistry registry() { + return (BasicBrooklynTypeRegistry) mgmt().getTypeRegistry(); + } + + private void add(RegisteredType type) { + add(type, false); + } + private void add(RegisteredType type, boolean canForce) { + registry().addToLocalUnpersistedTypeRegistry(type, canForce); + } + + @YomlAllFieldsTopLevel + public static class ItemA { + String name; + @Override public String toString() { return super.toString()+"[name="+name+"]"; } + } + + private final static RegisteredType SAMPLE_TYPE_BASE = BrooklynYomlTypeRegistry.newYomlRegisteredType( + RegisteredTypeKind.BEAN, "item-base", "1", ItemA.class); + + private final static RegisteredType SAMPLE_TYPE_TEST = BrooklynYomlTypeRegistry.newYomlRegisteredType( + RegisteredTypeKind.BEAN, "item-w-dsl", "1", "{ type: item-base, name: '$brooklyn:self().attributeWhenReady(\"test.sensor\")' }", + ItemA.class, null, null); + + @Test + public void testYomlParserRespectsDsl() throws Exception { + add(SAMPLE_TYPE_BASE); + add(SAMPLE_TYPE_TEST); + + String yaml = Joiner.on("\n").join( + "services:", + "- type: org.apache.brooklyn.core.test.entity.TestEntity", + " brooklyn.config:", + " test.obj:", + // with this, the yoml is resolved at retrieval time + " $brooklyn:object-yoml: item-w-dsl"); + + Entity app = createStartWaitAndLogApplication(yaml); + Entity entity = Iterables.getOnlyElement( app.getChildren() ); + + entity.sensors().set(Sensors.newStringSensor("test.sensor"), "bob"); + Maybe raw = ((EntityInternal)entity).config().getRaw(ConfigKeys.newConfigKey(Object.class, "test.obj")); + Asserts.assertPresent(raw); + Asserts.assertInstanceOf(raw.get(), Supplier.class); + Object obj = entity.config().get(ConfigKeys.newConfigKey(Object.class, "test.obj")); + Assert.assertEquals(((ItemA)obj).name, "bob"); + } + + @Test + public void testYomlDefersDslEvaluationForConfig() throws Exception { + add(SAMPLE_TYPE_BASE); + add(SAMPLE_TYPE_TEST); + YomlInitializers.install(mgmt()); + + String yaml = Joiner.on("\n").join( + "services:", + "- type: org.apache.brooklyn.core.test.entity.TestEntity", + " brooklyn.initializers:", + " a-sensor:", + " type: static-sensor", + " value: '$brooklyn:self().attributeWhenReady(\"test.sensor\")'", + " period: 100ms"); + + Entity app = createStartWaitAndLogApplication(yaml); + Entity entity = Iterables.getOnlyElement( app.getChildren() ); + + entity.sensors().set(Sensors.newStringSensor("test.sensor"), "bob"); +// EntityAsserts.assertAttributeEqualsEventually(entity, attribute, expected); + System.out.println(entity.getAttribute(Sensors.newStringSensor("a-sensor"))); + Time.sleep(Duration.ONE_SECOND); + System.out.println(entity.getAttribute(Sensors.newStringSensor("a-sensor"))); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTestFixture.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTestFixture.java new file mode 100644 index 0000000000..8cb34c6a0f --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/BrooklynYomlTestFixture.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.annotations.YomlAnnotations; +import org.apache.brooklyn.util.yoml.tests.YomlTestFixture; + +public class BrooklynYomlTestFixture extends YomlTestFixture { + + public static YomlTestFixture newInstance() { return new BrooklynYomlTestFixture(); } + public static YomlTestFixture newInstance(YomlConfig config) { return new BrooklynYomlTestFixture(config); } + public static YomlTestFixture newInstance(ManagementContext mgmt) { + return newInstance(YomlTypePlanTransformer.newYomlConfig(mgmt, null).build()); + } + + public BrooklynYomlTestFixture() {} + public BrooklynYomlTestFixture(YomlConfig config) { + super(config); + } + + @Override + protected YomlAnnotations annotationsProvider() { + return new BrooklynYomlAnnotations(); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/ObjectYomlInBrooklynDslTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/ObjectYomlInBrooklynDslTest.java new file mode 100644 index 0000000000..e580852c23 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/ObjectYomlInBrooklynDslTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; + +public class ObjectYomlInBrooklynDslTest extends AbstractYamlTest { + + private static final Logger log = LoggerFactory.getLogger(ObjectYomlInBrooklynDslTest.class); + + private BasicBrooklynTypeRegistry registry() { + return (BasicBrooklynTypeRegistry) mgmt().getTypeRegistry(); + } + + private void add(RegisteredType type) { + add(type, false); + } + private void add(RegisteredType type, boolean canForce) { + registry().addToLocalUnpersistedTypeRegistry(type, canForce); + } + + @YomlAllFieldsTopLevel + public static class ItemA { + String name; + /* required for 'object.fields' */ public void setName(String name) { this.name = name; } + @Override public String toString() { return super.toString()+"[name="+name+"]"; } + } + + private final static RegisteredType SAMPLE_TYPE = BrooklynYomlTypeRegistry.newYomlRegisteredType( + RegisteredTypeKind.BEAN, null, "1", ItemA.class); + + Entity doDeploy(String ...lines) throws Exception { + add(SAMPLE_TYPE); + String yaml = Joiner.on("\n").join( + "services:", + "- type: org.apache.brooklyn.core.test.entity.TestEntity", + " brooklyn.config:", + " test.obj:"); + yaml += Joiner.on("\n ").join("", "", (Object[])lines); + + Entity app = createStartWaitAndLogApplication(yaml); + return Iterables.getOnlyElement( app.getChildren() ); + } + + void doTest(String ...lines) throws Exception { + Entity entity = doDeploy(lines); + Object obj = entity.config().get(ConfigKeys.newConfigKey(Object.class, "test.obj")); + log.info("Object for "+JavaClassNames.callerNiceClassAndMethod(1)+" : "+obj); + Asserts.assertInstanceOf(obj, ItemA.class); + Assert.assertEquals(((ItemA)obj).name, "bob"); + } + + @Test + public void testOldStyle() throws Exception { + doTest( + "$brooklyn:object:", + " type: "+ItemA.class.getName(), + " object.fields:", + " name: bob"); + } + + @Test + public void testYomlSyntax() throws Exception { + doTest( + "$brooklyn:object-yoml:", + " type: "+ItemA.class.getName(), + " name: bob"); + } + + @Test + public void testYomlSyntaxUsindDslAgain() throws Exception { + Entity entity = doDeploy( + "$brooklyn:object-yoml:", + " type: "+ItemA.class.getName(), + " name: $brooklyn:self().attributeWhenReady(\"test.sensor\")"); + entity.sensors().set(Sensors.newStringSensor("test.sensor"), "bobella"); + Object obj = entity.config().get(ConfigKeys.newConfigKey(Object.class, "test.obj")); + Assert.assertEquals(((ItemA)obj).name, "bobella"); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlConfigKeyInheritanceTests.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlConfigKeyInheritanceTests.java new file mode 100644 index 0000000000..c49b166185 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlConfigKeyInheritanceTests.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.BasicConfigInheritance; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; +import org.apache.brooklyn.util.yoml.tests.YomlTestFixture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.reflect.TypeToken; + +/** Tests that config key inheritance strategies are obeyed when reading with supertypes. + */ +public class YomlConfigKeyInheritanceTests { + + private static final Logger log = LoggerFactory.getLogger(YomlConfigKeyInheritanceTests.class); + + @YomlConfigMapConstructor("conf") + static class M0 { + Map conf = MutableMap.of(); + M0(Map keys) { this.conf.putAll(keys); } + + @Override + public boolean equals(Object obj) { + return (obj instanceof M0) && ((M0)obj).conf.equals(conf); + } + @Override + public int hashCode() { + return conf.hashCode(); + } + @Override + public String toString() { + return super.toString()+conf; + } + } + + static class M1 extends M0 { + @SuppressWarnings("serial") + static ConfigKey> KM = ConfigKeys.builder(new TypeToken>() {}, "km") + .typeInheritance(BasicConfigInheritance.DEEP_MERGE).build(); + + @SuppressWarnings("serial") + static ConfigKey> KO = ConfigKeys.builder(new TypeToken>() {}, "ko") + .typeInheritance(BasicConfigInheritance.OVERWRITE).build(); + + M1(Map keys) { super(keys); } + } + + @Test + public void testReadMergedMap() { + YomlTestFixture y = BrooklynYomlTestFixture.newInstance().addTypeWithAnnotations("m1", M1.class) + .addType("m1a", "{ type: m1, km: { a: 1, b: 1 }, ko: { a: 1, b: 1 } }") + .addType("m1b", "{ type: m1a, km: { b: 2 }, ko: { b: 2 } }"); + + y.read("{ type: m1b }", null); + + M1 m1b = (M1)y.getLastReadResult(); + Asserts.assertEquals(m1b.conf.get(M1.KM.getName()), MutableMap.of("a", 1, "b", 2)); + Asserts.assertEquals(m1b.conf.get(M1.KO.getName()), MutableMap.of("b", 2)); + } + + @Test + public void testWrite() { + // the write is not smart enough to look at default/inherited KV pairs + // (this would be nice to change, but a lot of work and not really worth it) + + YomlTestFixture y = YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("m1", M1.class, MutableMap.of("keys", "config")); + + M1 m1b = new M1(MutableMap.of("km", MutableMap.of("a", 1, "b", 2), "ko", MutableMap.of("a", 1, "b", 2))); + y.write(m1b); + log.info("written as "+y.getLastWriteResult()); + y.assertLastWriteIgnoringQuotes( + "{ type=m1, km:{ a: 1, b: 2 }, ko: { a: 1, b: 2 }}", "wrong serialization"); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryBasicTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryBasicTest.java new file mode 100644 index 0000000000..488867f680 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryBasicTest.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.Arrays; +import java.util.Map; + +import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.RegisteredType; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.test.BrooklynMgmtUnitTestSupport; +import org.apache.brooklyn.core.typereg.BasicBrooklynTypeRegistry; +import org.apache.brooklyn.core.typereg.JavaClassNameTypePlanTransformer.JavaClassNameTypeImplementationPlan; +import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.yoml.YomlConfigBagConstructor; +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class YomlTypeRegistryBasicTest extends BrooklynMgmtUnitTestSupport { + + private BasicBrooklynTypeRegistry registry() { + return (BasicBrooklynTypeRegistry) mgmt.getTypeRegistry(); + } + + private void add(RegisteredType type) { + add(type, false); + } + private void add(RegisteredType type, boolean canForce) { + registry().addToLocalUnpersistedTypeRegistry(type, canForce); + } + + public static class ItemA { + String name; + } + + private final static RegisteredType SAMPLE_TYPE_JAVA = RegisteredTypes.bean("java.A", "1", + new JavaClassNameTypeImplementationPlan(ItemA.class.getName()), ItemA.class); + + @Test + public void testInstantiateYomlPlan() { + add(SAMPLE_TYPE_JAVA); + Object x = registry().createBeanFromPlan("yoml", "{ type: java.A }", null, null); + Assert.assertTrue(x instanceof ItemA); + } + + @Test + public void testInstantiateYomlPlanExplicitField() { + add(SAMPLE_TYPE_JAVA); + Object x = registry().createBeanFromPlan("yoml", "{ type: java.A, fields: { name: Bob } }", null, null); + Assert.assertTrue(x instanceof ItemA); + Assert.assertEquals( ((ItemA)x).name, "Bob" ); + } + + private static RegisteredType sampleTypeYoml(String typeName, String typeDefName) { + return BrooklynYomlTypeRegistry.newYomlRegisteredType(RegisteredTypeKind.BEAN, + // symbolicName, version, + typeName==null ? "yoml.A" : typeName, "1", + // planData, + "{ type: "+ (typeDefName==null ? ItemA.class.getName() : typeDefName) +" }", + // javaConcreteType, superTypesAsClassOrRegisteredType, serializers) + ItemA.class, Arrays.asList(ItemA.class), null); + } + private final static RegisteredType SAMPLE_TYPE_YOML = sampleTypeYoml(null, null); + + @Test + public void testInstantiateYomlBaseType() { + add(SAMPLE_TYPE_YOML); + Object x = registry().createBeanFromPlan("yoml", "{ type: yoml.A }", null, null); + Assert.assertTrue(x instanceof ItemA); + } + + @Test + public void testInstantiateYomlBaseTypeJavaPrefix() { + add(sampleTypeYoml(null, "'java:"+ItemA.class.getName()+"'")); + Object x = registry().createBeanFromPlan("yoml", "{ type: yoml.A }", null, null); + Assert.assertTrue(x instanceof ItemA); + } + + @Test + public void testInstantiateYomlBaseTypeSameName() { + add(sampleTypeYoml(ItemA.class.getName(), null)); + Object x = registry().createBeanFromPlan("yoml", "{ type: "+ItemA.class.getName()+" }", null, null); + Assert.assertTrue(x instanceof ItemA); + } + + @Test + public void testInstantiateYomlBaseTypeExplicitField() { + add(SAMPLE_TYPE_YOML); + Object x = registry().createBeanFromPlan("yoml", "{ type: yoml.A, fields: { name: Bob } }", null, null); + Assert.assertTrue(x instanceof ItemA); + Assert.assertEquals( ((ItemA)x).name, "Bob" ); + } + + @Test + public void testYomlTypeMissingGiveGoodError() { + try { + Object x = registry().createBeanFromPlan("yoml", "{ type: yoml.A, fields: { name: Bob } }", null, null); + Asserts.shouldHaveFailedPreviously("Expected type resolution failure; instead it loaded "+x); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "yoml.A", "neither", "registry", "classpath"); + Asserts.expectedFailureDoesNotContain(e, JavaClassNames.simpleClassName(ClassNotFoundException.class)); + } + } + + @Test + public void testYomlTypeMissingGiveGoodErrorNested() { + add(sampleTypeYoml("yoml.B", "yoml.A")); + try { + Object x = registry().createBeanFromPlan("yoml", "{ type: yoml.B, fields: { name: Bob } }", null, null); + Asserts.shouldHaveFailedPreviously("Expected type resolution failure; instead it loaded "+x); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "yoml.B", "yoml.A", "neither", "registry", "classpath"); + Asserts.expectedFailureDoesNotContain(e, JavaClassNames.simpleClassName(ClassNotFoundException.class)); + } + } + + @YomlAllFieldsTopLevel + @Alias("item-annotated") + public static class ItemAn { + final static RegisteredType YOML = BrooklynYomlTypeRegistry.newYomlRegisteredType(RegisteredTypeKind.BEAN, + null, "1", ItemAn.class); + + String name; + } + + @Test + public void testInstantiateAnnotatedYoml() { + add(ItemAn.YOML); + Object x = registry().createBeanFromPlan("yoml", "{ type: item-annotated, name: bob }", null, null); + Assert.assertTrue(x instanceof ItemAn); + Assert.assertEquals( ((ItemAn)x).name, "bob" ); + } + + + @YomlConfigBagConstructor(value="config", writeAsKey="extraConfig") + static class ConfigurableExampleFromBag { + ConfigBag config = ConfigBag.newInstance(); + ConfigurableExampleFromBag(ConfigBag bag) { config.putAll(bag); } + static ConfigKey S = ConfigKeys.newStringConfigKey("s"); + static ConfigKey CB = ConfigKeys.newConfigKey(ConfigurableExampleFromBag.class, "bag"); + @Override + public boolean equals(Object obj) { + return (getClass().equals(obj.getClass())) && config.getAllConfig().equals(((ConfigurableExampleFromBag)obj).config.getAllConfig()); + } + @Override + public String toString() { + return super.toString()+"["+config+"]"; + } + } + + @Test + public void testReadWriteAnnotation() { + BrooklynYomlTestFixture.newInstance() + .addTypeWithAnnotations("bag-example", ConfigurableExampleFromBag.class) + .reading("{ type: bag-example, s: foo }").writing( + new ConfigurableExampleFromBag(ConfigBag.newInstance().configure(ConfigurableExampleFromBag.S, "foo"))) + .doReadWriteAssertingJsonMatch(); + } + + static class ConfigurableExampleFromMap extends ConfigurableExampleFromBag { + ConfigurableExampleFromMap(Map bag) { super(ConfigBag.newInstance(bag)); } + } + + @Test + public void testReadWriteAnnotationMapConstructorInherited() { + BrooklynYomlTestFixture.newInstance() + .addTypeWithAnnotations("bag-example", ConfigurableExampleFromMap.class) + .reading("{ type: bag-example, s: foo }").writing( + new ConfigurableExampleFromMap(ConfigBag.newInstance().configure(ConfigurableExampleFromBag.S, "foo").getAllConfig())) + .doReadWriteAssertingJsonMatch(); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryEntityInitializersTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryEntityInitializersTest.java new file mode 100644 index 0000000000..2a57797871 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/YomlTypeRegistryEntityInitializersTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.entity.EntityInitializer; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.camp.yoml.types.YomlInitializers; +import org.apache.brooklyn.core.sensor.DependentConfiguration; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.core.sensor.StaticSensor; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.yaml.Yamls; +import org.apache.brooklyn.util.yoml.tests.YomlTestFixture; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; + +public class YomlTypeRegistryEntityInitializersTest extends AbstractYamlTest { + + @BeforeMethod(alwaysRun = true) + @Override + public void setUp() throws Exception { + super.setUp(); + + // TODO logically how should we populate the catalog? see notes on the method called below + YomlInitializers.install(mgmt()); + } + + StaticSensor SS_42 = new StaticSensor(ConfigBag.newInstance() + .configure(StaticSensor.SENSOR_NAME, "the-answer") + .configure(StaticSensor.SENSOR_TYPE, "int") + .configure(StaticSensor.STATIC_VALUE, 42) ); + + // permitted anytime + final static String SS_42_YAML_SIMPLE = Joiner.on("\n").join( + "name: the-answer", + "type: static-sensor", + "sensor-type: int", + "value: 42"); + + // permitted if we know we are reading EntityInitializer instances + final String SS_42_YAML_SINGLETON_MAP = Joiner.on("\n").join( + "the-answer:", + " type: static-sensor", + " sensor-type: int", + " value: 42"); + + @Test + public void testYomlReadSensor() throws Exception { + String yaml = Joiner.on("\n").join( + "name: the-answer", + "type: static-sensor", + "sensor-type: int", + "value: 42"); + + Object ss = mgmt().getTypeRegistry().createBeanFromPlan("yoml", Yamls.parseAll(yaml).iterator().next(), null, null); + Asserts.assertInstanceOf(ss, StaticSensor.class); + // Assert.assertEquals(ss, SS_42); // StaticSensor does not support equals + } + + @Test + public void testYomlReadSensorWithExpectedSuperType() throws Exception { + Object ss = mgmt().getTypeRegistry().createBeanFromPlan("yoml", Yamls.parseAll(SS_42_YAML_SIMPLE).iterator().next(), null, EntityInitializer.class); + Asserts.assertInstanceOf(ss, StaticSensor.class); + // Assert.assertEquals(ss, SS_42); // StaticSensor does not support equals + } + + @Test + public void testReadSensorAsMapWithName() throws Exception { + Object ss = mgmt().getTypeRegistry().createBeanFromPlan("yoml", Yamls.parseAll(SS_42_YAML_SINGLETON_MAP).iterator().next(), null, EntityInitializer.class); + Asserts.assertInstanceOf(ss, StaticSensor.class); + // Assert.assertEquals(ss, SS_42); // StaticSensor does not support equals + } + + @Test + public void testYomlReadSensorSingletonMapWithFixture() throws Exception { + YomlTestFixture y = BrooklynYomlTestFixture.newInstance(mgmt()); + y.read(SS_42_YAML_SINGLETON_MAP, "entity-initializer"); + Asserts.assertInstanceOf(y.getLastReadResult(), StaticSensor.class); + } + + @Test + public void testYomlWriteSensorWithFixture() throws Exception { + YomlTestFixture y = BrooklynYomlTestFixture.newInstance(mgmt()); + y.write(SS_42, "entity-initializer").assertLastWriteIgnoringQuotes( + "{the-answer: { type: static-sensor, period: 5m, targetType: int, timeout: a very long time, value: 42}}" + // ideally it would be simple like below but StaticSensor sets all the values + // so it ends up looking like the above; not too bad +// Jsonya.newInstance().add( Yamls.parseAll(SS_42_YAML_SINGLETON_MAP).iterator().next() ).toString() + ); + + // and another read/write cycle gives the same thing + Object fullOutput = y.getLastWriteResult(); + y.readLastWrite().writeLastRead(); + Assert.assertEquals(fullOutput, y.getLastWriteResult()); + } + + // and test in context + + @Test(enabled=false) // this format (list) still runs old camp parse, does not attempt yaml, included for comparison + public void testStaticSensorWorksAsList() throws Exception { + String yaml = Joiner.on("\n").join( + "services:", + "- type: org.apache.brooklyn.core.test.entity.TestEntity", + " brooklyn.initializers:", + " - name: the-answer", + " type: static-sensor", + " sensor-type: int", + " value: 42"); + + checkStaticSensorInApp(yaml); + } + + @Test + public void testStaticSensorWorksAsSingletonMap() throws Exception { + String yaml = Joiner.on("\n").join( + "services:", + "- type: org.apache.brooklyn.core.test.entity.TestEntity", + " brooklyn.initializers:", + " the-answer:", + " type: static-sensor", + " sensor-type: int", + " value: 42"); + + checkStaticSensorInApp(yaml); + } + + protected void checkStaticSensorInApp(String yaml) + throws Exception, InterruptedException, ExecutionException, TimeoutException { + final Entity app = createStartWaitAndLogApplication(yaml); + TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren()); + + Assert.assertEquals( + entity.getExecutionContext().submit( + DependentConfiguration.attributeWhenReady(entity, Sensors.newIntegerSensor("the-answer")) ) + .get( Duration.FIVE_SECONDS ), (Integer) 42); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/demos/YomlSensorEffectorTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/demos/YomlSensorEffectorTest.java new file mode 100644 index 0000000000..4bd458f485 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/yoml/demos/YomlSensorEffectorTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.yoml.demos; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.camp.yoml.types.YomlInitializers; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.effector.Effectors; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess; +import org.apache.brooklyn.util.collections.MutableMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class YomlSensorEffectorTest extends AbstractYamlTest { + + private static final Logger log = LoggerFactory.getLogger(YomlSensorEffectorTest.class); + + protected EmptySoftwareProcess startApp() throws Exception { + YomlInitializers.install(mgmt()); + + Entity app = createAndStartApplication(loadYaml("ssh-sensor-effector-yoml-demo.yaml")); + waitForApplicationTasks(app); + + log.info("App started:"); + Entities.dumpInfo(app); + + EmptySoftwareProcess entity = (EmptySoftwareProcess) app.getChildren().iterator().next(); + return entity; + } + + @Test(groups="Integration") + public void testBasicEffector() throws Exception { + EmptySoftwareProcess entity = startApp(); + + String name = entity.getConfig(ConfigKeys.newStringConfigKey("name")); + Assert.assertEquals(name, "bob"); + + Task hi = entity.invoke(Effectors.effector(String.class, "echo-hi").buildAbstract(), MutableMap.of()); + Assert.assertEquals(hi.get().trim(), "hi"); + } + + @Test(groups="Integration") + public void testConfigEffector() throws Exception { + EmptySoftwareProcess entity = startApp(); + + String name = entity.getConfig(ConfigKeys.newStringConfigKey("name")); + Assert.assertEquals(name, "bob"); + + Task hi = entity.invoke(Effectors.effector(String.class, "echo-hi-name-from-config").buildAbstract(), MutableMap.of()); + Assert.assertEquals(hi.get().trim(), "hi bob"); + } + + @Test(groups="Integration") + public void testSensorAndEffector() throws Exception { + EmptySoftwareProcess entity = startApp(); + + EntityAsserts.assertAttributeChangesEventually(entity, Sensors.newStringSensor("date")); + } + +} \ No newline at end of file diff --git a/camp/camp-brooklyn/src/test/resources/ssh-sensor-effector-yoml-demo.yaml b/camp/camp-brooklyn/src/test/resources/ssh-sensor-effector-yoml-demo.yaml new file mode 100644 index 0000000000..1bc996962f --- /dev/null +++ b/camp/camp-brooklyn/src/test/resources/ssh-sensor-effector-yoml-demo.yaml @@ -0,0 +1,41 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +location: localhost + +services: + +- type: org.apache.brooklyn.entity.software.base.EmptySoftwareProcess + + brooklyn.initializers: + echo-hi: + type: ssh-effector + command: echo hi + echo-hi-name-from-config: + type: ssh-effector + command: echo hi ${NAME} + env: + NAME: $brooklyn:config("name") + date: + type: ssh-sensor + command: date + period: 100ms + + brooklyn.config: + name: bob diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java index 636742b3a3..0b01419be3 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java @@ -69,8 +69,10 @@ import org.apache.brooklyn.core.typereg.BasicRegisteredType; import org.apache.brooklyn.core.typereg.BasicTypeImplementationPlan; import org.apache.brooklyn.core.typereg.BrooklynTypePlanTransformer; +import org.apache.brooklyn.core.typereg.RegisteredTypeInfo; import org.apache.brooklyn.core.typereg.RegisteredTypeNaming; import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.core.typereg.TypePlanTransformers; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; @@ -1799,11 +1801,21 @@ public ReferenceWithError resolve(RegisteredType typeToValidate, } RegisteredTypes.cacheActualJavaType(resultT, resultS); - Set newSupers = MutableSet.of(); - // TODO collect registered type name supertypes, as strings + MutableSet newSupers = MutableSet.of(); newSupers.add(resultS); + + Maybe typeInfo = TypePlanTransformers.getTypeInfo(mgmt, resultT, constraint); + if (typeInfo.isPresent()) { + Set supersTI = typeInfo.get().getSupertypes(); + if (supersTI!=null) { + newSupers.addAll(supersTI); + } + } + + // following should be redundant if the above worked, but the + // above might not have worked, and no harm in any case newSupers.addAll(supers); - newSupers.add(BrooklynObjectType.of(resultO.getClass()).getInterfaceType()); + newSupers.addIfNotNull(BrooklynObjectType.of(resultO.getClass()).getInterfaceType()); collectSupers(newSupers); RegisteredTypes.addSuperTypes(resultT, newSupers); diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java index a25e19ddab..ae4df3a3b4 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java @@ -87,7 +87,7 @@ public static BrooklynClassLoadingContext newClassLoadingContext(ManagementConte public static BrooklynClassLoadingContext newClassLoadingContext(ManagementContext mgmt, RegisteredType item) { return newClassLoadingContext(mgmt, item.getId(), item.getLibraries(), null); } - + /** made @Beta in 0.9.0 because we're not sure to what extent to support stacking loaders; * only a couple places currently rely on such stacking, in general the item and the bundles *are* the context, * and life gets hard if we support complex stacking! */ diff --git a/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java index b2ec95d748..d87d4def68 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/BasicConfigKey.java @@ -360,16 +360,12 @@ public ConfigInheritance getInheritance() { @Deprecated @Override @Nullable public ConfigInheritance getTypeInheritance() { - return typeInheritance; + return getInheritanceByContext(InheritanceContext.TYPE_DEFINITION); } @Deprecated @Override @Nullable public ConfigInheritance getParentInheritance() { - if (parentInheritance == null && inheritance != null) { - parentInheritance = inheritance; - inheritance = null; - } - return parentInheritance; + return getInheritanceByContext(InheritanceContext.RUNTIME_MANAGEMENT); } /** @see ConfigKey#getConstraint() */ diff --git a/core/src/main/java/org/apache/brooklyn/core/config/ListConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/ListConfigKey.java index 7a957281c4..78026ac790 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/ListConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/ListConfigKey.java @@ -18,6 +18,8 @@ */ package org.apache.brooklyn.core.config; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import static com.google.common.base.Preconditions.checkNotNull; import java.util.ArrayList; @@ -66,12 +68,13 @@ public class ListConfigKey extends AbstractCollectionConfigKey,List extends BasicConfigKey.Builder,Builder> { protected Class subType; + @SuppressWarnings("unchecked") public Builder(TypeToken subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenListWithSubtype((Class)subType.getRawType()), name); this.subType = (Class) subType.getRawType(); } public Builder(Class subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenListWithSubtype(subType), name); this.subType = checkNotNull(subType, "subType"); } public Builder(ListConfigKey key) { @@ -118,11 +121,31 @@ public ListConfigKey(Class subType, String name, String description) { this(subType, name, description, null); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("unchecked") public ListConfigKey(Class subType, String name, String description, List defaultValue) { - super((Class)List.class, subType, name, description, (List)defaultValue); + super(typeTokenListWithSubtype(subType), subType, name, description, (List) defaultValue); } + @SuppressWarnings("unchecked") + private static TypeToken> typeTokenListWithSubtype(final Class subType) { + return (TypeToken>) TypeToken.of(new ParameterizedType() { + @Override + public Type getRawType() { + return List.class; + } + + @Override + public Type getOwnerType() { + return null; + } + + @Override + public Type[] getActualTypeArguments() { + return new Type[] { subType }; + } + }); + } + @Override public String toString() { return String.format("%s[ListConfigKey:%s]", name, getTypeName()); @@ -143,7 +166,7 @@ public static class ListModifications extends StructuredModifications { /** when passed as a value to a ListConfigKey, causes each of these items to be added. * if you have just one, no need to wrap in a mod. */ // to prevent confusion (e.g. if a list is passed) we require two objects here. - public static final ListModification add(final T o1, final T o2, final T ...oo) { + public static final ListModification add(final T o1, final T o2, @SuppressWarnings("unchecked") final T ...oo) { List l = new ArrayList(); l.add(o1); l.add(o2); for (T o: oo) l.add(o); diff --git a/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java index e213bf76e6..2536e8ff25 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/MapConfigKey.java @@ -20,6 +20,8 @@ import static com.google.common.base.Preconditions.checkNotNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -68,12 +70,13 @@ public static Builder builder(MapConfigKey key) { public static class Builder extends BasicConfigKey.Builder,Builder> { protected Class subType; + @SuppressWarnings("unchecked") public Builder(TypeToken subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenMapWithSubtype( (Class) subType.getRawType() ), name); this.subType = (Class) subType.getRawType(); } public Builder(Class subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenMapWithSubtype(subType), name); this.subType = checkNotNull(subType, "subType"); } public Builder(MapConfigKey key) { @@ -112,6 +115,26 @@ protected MapConfigKey(Builder builder) { super(builder, builder.subType); } + @SuppressWarnings("unchecked") + private static TypeToken> typeTokenMapWithSubtype(final Class subType) { + return (TypeToken>) TypeToken.of(new ParameterizedType() { + @Override + public Type getRawType() { + return Map.class; + } + + @Override + public Type getOwnerType() { + return null; + } + + @Override + public Type[] getActualTypeArguments() { + return new Type[] { String.class, subType }; + } + }); + } + public MapConfigKey(Class subType, String name) { this(subType, name, name, null); } @@ -123,9 +146,8 @@ public MapConfigKey(Class subType, String name, String description) { // TODO it isn't clear whether defaultValue is an initialValue, or a value to use when map is empty // probably the latter, currently ... but maybe better to say that map configs are never null, // and defaultValue is really an initial value? - @SuppressWarnings({ "unchecked", "rawtypes" }) public MapConfigKey(Class subType, String name, String description, Map defaultValue) { - super((Class)Map.class, subType, name, description, defaultValue); + super(typeTokenMapWithSubtype(subType), subType, name, description, defaultValue); } @Override diff --git a/core/src/main/java/org/apache/brooklyn/core/config/SetConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/SetConfigKey.java index 495a82c8e6..61f7f3e768 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/SetConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/SetConfigKey.java @@ -20,6 +20,8 @@ import static com.google.common.base.Preconditions.checkNotNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; @@ -57,12 +59,13 @@ public class SetConfigKey extends AbstractCollectionConfigKey, Set extends BasicConfigKey.Builder,Builder> { protected Class subType; + @SuppressWarnings("unchecked") public Builder(TypeToken subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenSetWithSubtype((Class)subType.getRawType()), name); this.subType = (Class) subType.getRawType(); } public Builder(Class subType, String name) { - super(new TypeToken>() {}, name); + super(typeTokenSetWithSubtype(subType), name); this.subType = checkNotNull(subType, "subType"); } public Builder(SetConfigKey key) { @@ -109,11 +112,31 @@ public SetConfigKey(Class subType, String name, String description) { this(subType, name, description, null); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("unchecked") public SetConfigKey(Class subType, String name, String description, Set defaultValue) { - super((Class)Set.class, subType, name, description, (Set) defaultValue); + super(typeTokenSetWithSubtype(subType), subType, name, description, (Set)defaultValue); } + @SuppressWarnings("unchecked") + private static TypeToken> typeTokenSetWithSubtype(final Class subType) { + return (TypeToken>) TypeToken.of(new ParameterizedType() { + @Override + public Type getRawType() { + return Set.class; + } + + @Override + public Type getOwnerType() { + return null; + } + + @Override + public Type[] getActualTypeArguments() { + return new Type[] { subType }; + } + }); + } + @Override public String toString() { return String.format("%s[SetConfigKey:%s]", name, getTypeName()); @@ -134,7 +157,7 @@ public static class SetModifications extends StructuredModifications { /** when passed as a value to a SetConfigKey, causes each of these items to be added. * if you have just one, no need to wrap in a mod. */ // to prevent confusion (e.g. if a set is passed) we require two objects here. - public static final SetModification add(final T o1, final T o2, final T ...oo) { + public static final SetModification add(final T o1, final T o2, @SuppressWarnings("unchecked") final T ...oo) { Set l = new LinkedHashSet(); l.add(o1); l.add(o2); for (T o: oo) l.add(o); diff --git a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractCollectionConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractCollectionConfigKey.java index c7f977595b..eb432fc15a 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractCollectionConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractCollectionConfigKey.java @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; import com.google.common.collect.Iterables; +import com.google.common.reflect.TypeToken; public abstract class AbstractCollectionConfigKey, V> extends AbstractStructuredConfigKey { @@ -47,6 +48,10 @@ protected AbstractCollectionConfigKey(Class type, Class subType, String na super(type, subType, name, description, defaultValue); } + public AbstractCollectionConfigKey(TypeToken type, Class subType, String name, String description, T defaultValue) { + super(type, subType, name, description, defaultValue); + } + public ConfigKey subKey() { String subName = Identifiers.makeRandomId(8); return new SubElementConfigKey(this, subType, getName()+"."+subName, "element of "+getName()+", uid "+subName, null); diff --git a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java index d834283bed..88aff6489e 100644 --- a/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java +++ b/core/src/main/java/org/apache/brooklyn/core/config/internal/AbstractStructuredConfigKey.java @@ -29,6 +29,7 @@ import org.apache.brooklyn.util.exceptions.Exceptions; import com.google.common.collect.Maps; +import com.google.common.reflect.TypeToken; public abstract class AbstractStructuredConfigKey extends BasicConfigKey implements StructuredConfigKey { @@ -46,6 +47,11 @@ public AbstractStructuredConfigKey(Class type, Class subType, String name, this.subType = subType; } + public AbstractStructuredConfigKey(TypeToken type, Class subType, String name, String description, T defaultValue) { + super(type, name, description, defaultValue); + this.subType = subType; + } + protected ConfigKey subKey(String subName) { return subKey(subName, "sub-element of " + getName() + ", named " + subName); } diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/AddEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/AddEffector.java index 04c136433c..ab089fcdc6 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/AddEffector.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/AddEffector.java @@ -31,7 +31,10 @@ import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.yoml.YomlConfigBagConstructor; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; @@ -56,6 +59,9 @@ * * @since 0.7.0 */ @Beta +@YomlConfigBagConstructor("") +@YomlAllFieldsTopLevel +@YomlRenameDefaultKey("name") public class AddEffector implements EntityInitializer { public static final ConfigKey EFFECTOR_NAME = ConfigKeys.newStringConfigKey("name"); diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java b/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java index 92cc4ec140..6312fd9682 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java @@ -18,6 +18,7 @@ */ package org.apache.brooklyn.core.effector; +import java.lang.reflect.Field; import java.util.Map; import org.apache.brooklyn.api.entity.Entity; @@ -30,9 +31,17 @@ import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.util.core.ClassLoaderUtils; import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.yoml.YomlConfigBagConstructor; +import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.annotations.Beta; import com.google.common.base.Preconditions; @@ -47,18 +56,25 @@ * @since 0.7.0 */ @Beta +@YomlConfigBagConstructor("") +@YomlAllFieldsTopLevel +@YomlRenameDefaultKey("name") public class AddSensor implements EntityInitializer { + private static final Logger log = LoggerFactory.getLogger(AddSensor.class); + public static final ConfigKey SENSOR_NAME = ConfigKeys.newStringConfigKey("name", "The name of the sensor to create"); public static final ConfigKey SENSOR_PERIOD = ConfigKeys.newConfigKey(Duration.class, "period", "Period, including units e.g. 1m or 5s or 200ms; default 5 minutes", Duration.FIVE_MINUTES); + @Alias({"sensor-type","value-type"}) public static final ConfigKey SENSOR_TYPE = ConfigKeys.newStringConfigKey("targetType", "Target type for the value; default String", "java.lang.String"); protected final String name; protected final Duration period; - protected final String type; + protected final String targetType; protected AttributeSensor sensor; - protected final ConfigBag params; - + + private ConfigBag extraParams; + public AddSensor(Map params) { this(ConfigBag.newInstance(params)); } @@ -66,18 +82,93 @@ public AddSensor(Map params) { public AddSensor(final ConfigBag params) { this.name = Preconditions.checkNotNull(params.get(SENSOR_NAME), "Name must be supplied when defining a sensor"); this.period = params.get(SENSOR_PERIOD); - this.type = params.get(SENSOR_TYPE); - this.params = params; + + this.targetType = params.get(SENSOR_TYPE); + this.type = null; } - + + protected void rememberUnusedParams(ConfigBag bag) { + saveExtraParams(bag, false); + } + protected void rememberAllParams(ConfigBag bag) { + saveExtraParams(bag, false); + } + private void saveExtraParams(ConfigBag bag, boolean justUnused) { + if (extraParams==null) { + extraParams = ConfigBag.newInstance(); + } + if (justUnused) { + extraParams.putAll(params.getUnusedConfig()); + } else { + this.extraParams.copy(bag); + } + } + protected ConfigBag getRememberedParams() { + if (params!=null) { + synchronized (this) { + readResolve(); + } + } + if (extraParams==null) return ConfigBag.newInstance(); + return extraParams; + } + @Override public void apply(EntityLocal entity) { sensor = newSensor(entity); ((EntityInternal) entity).getMutableEntityType().addSensor(sensor); } + // old names, for XML deserializaton compatiblity + private final String type; + /** @deprecated since 0.9.0 and semantics slightly different; accessors should use {@link #getRememberedParams()} */ + private ConfigBag params; + private Object readResolve() { + try { + if (type!=null) { + if (targetType==null) { + Field f = Reflections.findField(getClass(), "targetType"); + f.setAccessible(true); + f.set(this, type); + } else if (!targetType.equals(type)) { + throw new IllegalStateException("Incompatible target types found for "+this+": "+type+" vs "+targetType); + } + + Field f = Reflections.findField(getClass(), "type"); + f.setAccessible(true); + f.set(this, null); + } + + if (params!=null) { + if (extraParams==null) { + extraParams = params; + } else if (!extraParams.getAllConfigAsConfigKeyMap().equals(params.getAllConfigAsConfigKeyMap())) { + throw new IllegalStateException("Incompatible extra params found for "+this+": "+params+" vs "+extraParams); + } + params = null; + } + } catch (Exception e) { + throw Exceptions.propagate(e); + } + return this; + } + + private Object writeReplace() { + try { + // make this null if there's nothing + if (extraParams!=null && extraParams.isEmpty()) { + Field f = Reflections.findField(getClass(), "extraParams"); + f.setAccessible(true); + f.set(this, null); + } + } catch (Exception e) { + throw Exceptions.propagate(e); + } + return this; + } + private AttributeSensor newSensor(Entity entity) { - String className = getFullClassName(type); + String className = getFullClassName(targetType); Class clazz = getType(entity, className); return Sensors.newSensor(clazz, name); } diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java index 2df5094fe3..6993ebf29a 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/ssh/SshCommandEffector.java @@ -48,16 +48,20 @@ import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.annotations.Alias; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import com.google.common.collect.Maps; +@Alias(preferred="ssh-effector") public final class SshCommandEffector extends AddEffector { + @Alias({"script", "run"}) public static final ConfigKey EFFECTOR_COMMAND = ConfigKeys.newStringConfigKey("command"); public static final ConfigKey EFFECTOR_EXECUTION_DIR = SshCommandSensor.SENSOR_EXECUTION_DIR; - public static final MapConfigKey EFFECTOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT; + @Alias(preferred="env", value={"vars","variables","environment"}) + public static final MapConfigKey EFFECTOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT_STRING_VALUES; public enum ExecutionTarget { ENTITY, @@ -87,7 +91,7 @@ public static EffectorBuilder newEffectorBuilder(ConfigBag params) { protected static class Body extends EffectorBody { private final Effector effector; private final String command; - private final Map shellEnv; + private final Map shellEnv; private final String executionDir; private final ExecutionTarget executionTarget; @@ -162,7 +166,7 @@ public SshEffectorTaskFactory makePartialTaskFactory(ConfigBag params, E if (shellEnv != null) env.putAll(shellEnv); // Add the shell environment entries from our invocation - Map effectorEnv = params.get(EFFECTOR_SHELL_ENVIRONMENT); + Map effectorEnv = params.get(EFFECTOR_SHELL_ENVIRONMENT); if (effectorEnv != null) env.putAll(effectorEnv); // Try to resolve the configuration in the env Map diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java index f3dca6b9fd..e3af056a9e 100644 --- a/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java +++ b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java @@ -138,12 +138,21 @@ public class BrooklynConfigKeys { .runtimeInheritance(BasicConfigInheritance.NOT_REINHERITED) .build(); - public static final MapConfigKey SHELL_ENVIRONMENT = new MapConfigKey.Builder(Object.class, "shell.env") - .description("Map of environment variables to pass to the runtime shell. Non-string values are serialized to json before passed to the shell.") - .defaultValue(ImmutableMap.of()) - .typeInheritance(BasicConfigInheritance.DEEP_MERGE) - .runtimeInheritance(BasicConfigInheritance.NOT_REINHERITED_ELSE_DEEP_MERGE) - .build(); + public static final MapConfigKey SHELL_ENVIRONMENT_STRING_VALUES = new MapConfigKey.Builder(String.class, "shell.env") + .description("Map of environment variables to pass to the runtime shell") + .defaultValue(ImmutableMap.of()) + .typeInheritance(BasicConfigInheritance.DEEP_MERGE) + .runtimeInheritance(BasicConfigInheritance.NOT_REINHERITED_ELSE_DEEP_MERGE) + .build(); + + public static final MapConfigKey SHELL_ENVIRONMENT_OBJECT_VALUE = new MapConfigKey.Builder(Object.class, "shell.env") + .description("Map of environment variables to pass to the runtime shell. Non-string values are serialized to json before passed to the shell.") + .defaultValue(ImmutableMap.of()) + .typeInheritance(BasicConfigInheritance.DEEP_MERGE) + .runtimeInheritance(BasicConfigInheritance.NOT_REINHERITED_ELSE_DEEP_MERGE) + .build(); + + public static final MapConfigKey SHELL_ENVIRONMENT = SHELL_ENVIRONMENT_OBJECT_VALUE; // TODO these dirs should also not be reinherited at runtime public static final AttributeSensorAndConfigKey INSTALL_DIR = new TemplatedStringAttributeSensorAndConfigKey( diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/BrooklynClassLoadingContextSequential.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/BrooklynClassLoadingContextSequential.java index ddd5c5b364..bbc4310caf 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/BrooklynClassLoadingContextSequential.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/classloading/BrooklynClassLoadingContextSequential.java @@ -84,7 +84,7 @@ public Maybe> tryLoadClass(String className) { errors.add( Maybe.getException(clazz) ); } - return Maybe.absent(Exceptions.create("Unable to load "+className+" from "+primaries, errors)); + return Maybe.absent(Exceptions.create("Unable to load type "+className+" from "+primaries, errors)); } @Override diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/StaticSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/StaticSensor.java index 6a609fa728..64fa47155b 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/StaticSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/StaticSensor.java @@ -31,6 +31,9 @@ import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.apache.brooklyn.util.yoml.annotations.YomlTypeFromOtherField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,14 +49,17 @@ * which can be useful if the supplied value is such a function. * However when the source is another sensor, * consider using {@link Propagator} which listens for changes instead. */ +@Alias("static-sensor") public class StaticSensor extends AddSensor { private static final Logger log = LoggerFactory.getLogger(StaticSensor.class); + @Alias("value") @YomlTypeFromOtherField("targetType") public static final ConfigKey STATIC_VALUE = ConfigKeys.newConfigKey(Object.class, "static.value"); public static final ConfigKey TIMEOUT = ConfigKeys.newConfigKey( Duration.class, "static.timeout", "Duration to wait for the value to resolve", Duration.PRACTICALLY_FOREVER); + @YomlTypeFromOtherField("targetType") private final Object value; private final Duration timeout; diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/function/FunctionSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/function/FunctionSensor.java index 4b324511b3..605621183d 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/function/FunctionSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/function/FunctionSensor.java @@ -59,6 +59,7 @@ public final class FunctionSensor extends AddSensor { public FunctionSensor(final ConfigBag params) { super(params); + rememberUnusedParams(params); } @Override @@ -69,7 +70,7 @@ public void apply(final EntityLocal entity) { LOG.debug("Adding HTTP JSON sensor {} to {}", name, entity); } - final ConfigBag allConfig = ConfigBag.newInstanceCopying(this.params).putAll(params); + final ConfigBag allConfig = ConfigBag.newInstanceCopying(getRememberedParams()); final Callable function = EntityInitializers.resolve(allConfig, FUNCTION); final Boolean suppressDuplicates = EntityInitializers.resolve(allConfig, SUPPRESS_DUPLICATES); diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java index 842bcb4ef1..bf0190f1be 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java @@ -27,13 +27,11 @@ import org.apache.brooklyn.core.config.MapConfigKey; import org.apache.brooklyn.core.effector.AddSensor; import org.apache.brooklyn.core.entity.EntityInitializers; -import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.sensor.ssh.SshCommandSensor; import org.apache.brooklyn.feed.http.HttpFeed; import org.apache.brooklyn.feed.http.HttpPollConfig; import org.apache.brooklyn.feed.http.HttpValueFunctions; import org.apache.brooklyn.util.core.config.ConfigBag; -import org.apache.brooklyn.util.core.config.ResolvingConfigBag; import org.apache.brooklyn.util.http.HttpToolResponse; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; @@ -74,6 +72,10 @@ public final class HttpRequestSensor extends AddSensor { public HttpRequestSensor(final ConfigBag params) { super(params); + // TODO yoml serialization of this needs some attention; probably better to use a pure + // config bag approach (as in this class) rather than an "extract-in-constructor" (as in parent) + // so that there are no serialized fields, just serialized config + rememberUnusedParams(params); } @Override @@ -84,7 +86,7 @@ public void apply(final EntityLocal entity) { LOG.debug("Adding HTTP JSON sensor {} to {}", name, entity); } - final ConfigBag allConfig = ConfigBag.newInstanceCopying(this.params).putAll(params); + final ConfigBag allConfig = ConfigBag.newInstance().putAll(getRememberedParams()); // TODO Keeping anonymous inner class for backwards compatibility with persisted state new Supplier() { diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java index 2fd3f3f719..bd341afea5 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/ssh/SshCommandSensor.java @@ -43,6 +43,7 @@ import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.annotations.Alias; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,16 +61,18 @@ * * @see HttpRequestSensor */ -@Beta +@Alias("ssh-sensor") public final class SshCommandSensor extends AddSensor { private static final Logger LOG = LoggerFactory.getLogger(SshCommandSensor.class); + @Alias({"script", "run"}) public static final ConfigKey SENSOR_COMMAND = ConfigKeys.newStringConfigKey("command", "SSH command to execute for sensor"); public static final ConfigKey SENSOR_EXECUTION_DIR = ConfigKeys.newStringConfigKey("executionDir", "Directory where the command should run; " + "if not supplied, executes in the entity's run dir (or home dir if no run dir is defined); " + "use '~' to always execute in the home dir, or 'custom-feed/' to execute in a custom-feed dir relative to the run dir"); - public static final MapConfigKey SENSOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT; + @Alias(preferred="env", value={"vars","variables","environment"}) + public static final MapConfigKey SENSOR_SHELL_ENVIRONMENT = BrooklynConfigKeys.SHELL_ENVIRONMENT_STRING_VALUES; public static final ConfigKey SUPPRESS_DUPLICATES = ConfigKeys.newBooleanConfigKey( "suppressDuplicates", @@ -78,7 +81,7 @@ public final class SshCommandSensor extends AddSensor { protected final String command; protected final String executionDir; - protected final Map sensorEnv; + protected final Map sensorEnv; public SshCommandSensor(final ConfigBag params) { super(params); @@ -97,9 +100,10 @@ public void apply(final EntityLocal entity) { LOG.debug("Adding SSH sensor {} to {}", name, entity); } - final Boolean suppressDuplicates = EntityInitializers.resolve(params, SUPPRESS_DUPLICATES); + final Boolean suppressDuplicates = EntityInitializers.resolve(getRememberedParams(), SUPPRESS_DUPLICATES); Supplier> envSupplier = new Supplier>() { + @SuppressWarnings("unchecked") @Override public Map get() { if (entity == null) return ImmutableMap.of(); // See BROOKLYN-568 diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/AbstractTypePlanTransformer.java b/core/src/main/java/org/apache/brooklyn/core/typereg/AbstractTypePlanTransformer.java index 52ee199d7c..66fc3c020f 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/AbstractTypePlanTransformer.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/AbstractTypePlanTransformer.java @@ -103,6 +103,7 @@ public Object create(final RegisteredType type, final RegisteredTypeLoadingConte @Override protected Object visitSpec() { try { AbstractBrooklynObjectSpec result = createSpec(type, context); + if (result==null) throw new UnsupportedTypePlanException("Transformer returned null for "+type); result.stackCatalogItemId(type.getId()); return result; } catch (Exception e) { throw Exceptions.propagate(e); } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicBrooklynTypeRegistry.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicBrooklynTypeRegistry.java index 7dd24cf44b..d23692db7a 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicBrooklynTypeRegistry.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicBrooklynTypeRegistry.java @@ -215,22 +215,22 @@ public RegisteredType get(String symbolicName, String version) { } @Override - public RegisteredType get(String symbolicNameWithOptionalVersion, RegisteredTypeLoadingContext context) { - return getMaybe(symbolicNameWithOptionalVersion, context).orNull(); + public RegisteredType get(String symbolicNameOrAliasWithOptionalVersion, RegisteredTypeLoadingContext context) { + return getMaybe(symbolicNameOrAliasWithOptionalVersion, context).orNull(); } @Override - public Maybe getMaybe(String symbolicNameWithOptionalVersion, RegisteredTypeLoadingContext context) { + public Maybe getMaybe(String symbolicNameOrAliasWithOptionalVersion, RegisteredTypeLoadingContext context) { Maybe r1 = null; - if (RegisteredTypeNaming.isUsableTypeColonVersion(symbolicNameWithOptionalVersion) || + if (RegisteredTypeNaming.isUsableTypeColonVersion(symbolicNameOrAliasWithOptionalVersion) || // included through 0.12 so legacy type names are accepted (with warning) - CatalogUtils.looksLikeVersionedId(symbolicNameWithOptionalVersion)) { - String symbolicName = CatalogUtils.getSymbolicNameFromVersionedId(symbolicNameWithOptionalVersion); - String version = CatalogUtils.getVersionFromVersionedId(symbolicNameWithOptionalVersion); + CatalogUtils.looksLikeVersionedId(symbolicNameOrAliasWithOptionalVersion)) { + String symbolicName = CatalogUtils.getSymbolicNameFromVersionedId(symbolicNameOrAliasWithOptionalVersion); + String version = CatalogUtils.getVersionFromVersionedId(symbolicNameOrAliasWithOptionalVersion); r1 = getSingle(symbolicName, version, context); if (r1.isPresent()) return r1; } - Maybe r2 = getSingle(symbolicNameWithOptionalVersion, BrooklynCatalog.DEFAULT_VERSION, context); + Maybe r2 = getSingle(symbolicNameOrAliasWithOptionalVersion, BrooklynCatalog.DEFAULT_VERSION, context); if (r2.isPresent() || r1==null) return r2; return r1; } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BrooklynTypePlanTransformer.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BrooklynTypePlanTransformer.java index 1001268a74..cbe77f572c 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/BrooklynTypePlanTransformer.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BrooklynTypePlanTransformer.java @@ -18,7 +18,6 @@ */ package org.apache.brooklyn.core.typereg; -import java.util.List; import java.util.ServiceLoader; import javax.annotation.Nonnull; @@ -30,8 +29,6 @@ import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; import org.apache.brooklyn.core.mgmt.ManagementContextInjectable; -import com.google.common.annotations.Beta; - /** * Interface for use by schemes which provide the capability to transform plans * (serialized descriptions) to brooklyn objecs and specs. @@ -69,6 +66,7 @@ public interface BrooklynTypePlanTransformer extends ManagementContextInjectable *

* */ double scoreForType(@Nonnull RegisteredType type, @Nonnull RegisteredTypeLoadingContext context); + /** Creates a new instance of the indicated type, or throws if not supported; * this method is used by the {@link BrooklynTypeRegistry} when it creates instances, * so implementations must respect the {@link RegisteredTypeKind} semantics and the {@link RegisteredTypeLoadingContext} @@ -81,11 +79,14 @@ public interface BrooklynTypePlanTransformer extends ManagementContextInjectable * if they cannot instantiate the given {@link RegisteredType#getPlan()}. */ @Nullable Object create(@Nonnull RegisteredType type, @Nonnull RegisteredTypeLoadingContext context); - // TODO sketch methods for loading *catalog* definitions. note some potential overlap - // with BrooklynTypeRegistery.createXxxFromPlan - @Beta - double scoreForTypeDefinition(String formatCode, Object catalogData); - @Beta - List createFromTypeDefinition(String formatCode, Object catalogData); - + /** Returns extended info for the given type, as it would be understood by this + * transformer. This may be incomplete, empty or even null if the transformer does not support type info. + *

+ * The framework guarantees this will only be invoked when {@link #scoreForType(RegisteredType, RegisteredTypeLoadingContext)} + * has returned a positive value, and the same constraints on the inputs as for that method apply. + *

+ * Implementations should either return null or throw {@link UnsupportedTypePlanException} + * if they cannot instantiate the given {@link RegisteredType#getPlan()}. */ + @Nullable RegisteredTypeInfo getTypeInfo(RegisteredType type); + } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/JavaClassNameTypePlanTransformer.java b/core/src/main/java/org/apache/brooklyn/core/typereg/JavaClassNameTypePlanTransformer.java index 29a4ec181c..496b150789 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/JavaClassNameTypePlanTransformer.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/JavaClassNameTypePlanTransformer.java @@ -18,12 +18,11 @@ */ package org.apache.brooklyn.core.typereg; -import java.util.List; - import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec; import org.apache.brooklyn.api.objs.BrooklynObject; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; +import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.text.Identifiers; /** @@ -74,18 +73,10 @@ protected Object createBean(RegisteredType type, RegisteredTypeLoadingContext co private Class getType(RegisteredType type, RegisteredTypeLoadingContext context) throws Exception { return RegisteredTypes.loadActualJavaType((String)type.getPlan().getPlanData(), mgmt, type, context); } - - - // not supported as a catalog format (yet? should we?) - - @Override - public double scoreForTypeDefinition(String formatCode, Object catalogData) { - return 0; - } @Override - public List createFromTypeDefinition(String formatCode, Object catalogData) { - throw new UnsupportedTypePlanException("this transformer does not support YAML catalog additions"); + public RegisteredTypeInfo getTypeInfo(RegisteredType type) { + return RegisteredTypeInfo.create(type, this, null, MutableSet.of()); } } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeInfo.java b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeInfo.java new file mode 100644 index 0000000000..381e3d1004 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeInfo.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.typereg; + +import java.util.Set; + +import org.apache.brooklyn.api.typereg.RegisteredType; + +/** Includes metadata for types, if available. Varies by {@link BrooklynTypePlanTransformer}, + * with the main one (CAMP) being quite complete. */ +public class RegisteredTypeInfo { + + private final RegisteredType type; + + private String planText; + private Set supertypes; + + private RegisteredTypeInfo(RegisteredType type) { + this.type = type; + } + + public static RegisteredTypeInfo create(RegisteredType type, BrooklynTypePlanTransformer transformer, + String planText, Set someSupertypes) { + + return new RegisteredTypeInfo(type); + } + + public RegisteredType getType() { + return type; + } + + public String getPlanText() { + return planText; + } + + void setPlanText(String planText) { + this.planText = planText; + } + + // list of supertypes, as RegisteredType and Class instances + public Set getSupertypes() { + return supertypes; + } + + void setSupertypes(Set supertypes) { + this.supertypes = supertypes; + } + +} diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeLoadingContexts.java b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeLoadingContexts.java index 7eab62696e..0b8da1e8e5 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeLoadingContexts.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypeLoadingContexts.java @@ -18,8 +18,7 @@ */ package org.apache.brooklyn.core.typereg; -import groovy.xml.Entity; - +import java.util.Arrays; import java.util.Set; import javax.annotation.Nonnull; @@ -34,11 +33,14 @@ import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableSet; +import groovy.xml.Entity; + public class RegisteredTypeLoadingContexts { private static final Logger log = LoggerFactory.getLogger(RegisteredTypeLoadingContexts.class); @@ -48,7 +50,8 @@ public final static class BasicRegisteredTypeLoadingContext implements Registere @Nullable private RegisteredTypeKind kind; @Nullable private Class expectedSuperType; @Nonnull private Set encounteredTypes = ImmutableSet.of(); - @Nullable BrooklynClassLoadingContext loader; + @Nullable private BrooklynClassLoadingContext loader; + @Nullable private ConstructionInstruction constructorInstruction; private BasicRegisteredTypeLoadingContext() {} @@ -57,8 +60,9 @@ public BasicRegisteredTypeLoadingContext(@Nullable RegisteredTypeLoadingContext this.kind = source.getExpectedKind(); this.expectedSuperType = source.getExpectedJavaSuperType(); - this.encounteredTypes = source.getAlreadyEncounteredTypes(); + this.encounteredTypes = MutableSet.copyOf(source.getAlreadyEncounteredTypes()); this.loader = source.getLoader(); + this.constructorInstruction = source.getConstructorInstruction(); } @Override @@ -83,6 +87,11 @@ public BrooklynClassLoadingContext getLoader() { return loader; } + @Override + public ConstructionInstruction getConstructorInstruction() { + return constructorInstruction; + } + @Override public String toString() { return JavaClassNames.cleanSimpleClassName(this)+"["+kind+","+expectedSuperType+","+encounteredTypes+"]"; @@ -225,15 +234,29 @@ static Class expectedSuperType) { result.expectedSuperType = expectedSuperType; return this; } + public Builder addEncounteredTypes(String... encounteredTypes) { result.encounteredTypes.addAll(Arrays.asList(encounteredTypes)); return this; } + public Builder loader(BrooklynClassLoadingContext loader) { result.loader = loader; return this; } + public Builder constructorInstruction(ConstructionInstruction constructorInstruction) { result.constructorInstruction = constructorInstruction; return this; } + + public RegisteredTypeLoadingContext build() { return new BasicRegisteredTypeLoadingContext(result); } + } + } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/TypePlanTransformers.java b/core/src/main/java/org/apache/brooklyn/core/typereg/TypePlanTransformers.java index cc4845c286..699ec345ae 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/TypePlanTransformers.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/TypePlanTransformers.java @@ -41,6 +41,7 @@ import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -106,6 +107,19 @@ public static List forType(ManagementContext mgmt, * callers should generally use one of the create methods on {@link BrooklynTypeRegistry} rather than using this method directly. */ @Beta public static Maybe transform(ManagementContext mgmt, RegisteredType type, RegisteredTypeLoadingContext constraint) { + return applyAtTransformers(mgmt, type, constraint, (t) -> t.create(type, constraint)); + } + + /** transforms the given type to an instance, if possible + *

+ * callers should generally use one of the create methods on {@link BrooklynTypeRegistry} rather than using this method directly. */ + @Beta + public static Maybe getTypeInfo(ManagementContext mgmt, RegisteredType type, RegisteredTypeLoadingContext constraint) { + return applyAtTransformers(mgmt, type, constraint, (t) -> t.getTypeInfo(type)); + } + + private static Maybe applyAtTransformers(ManagementContext mgmt, RegisteredType type, RegisteredTypeLoadingContext constraint, + Function fn) { if (type==null) return Maybe.absent("type cannot be null"); if (type.getPlan()==null) return Maybe.absent("type plan cannot be null, when instantiating "+type); @@ -114,7 +128,7 @@ public static Maybe transform(ManagementContext mgmt, RegisteredType typ Collection failuresFromTransformers = new ArrayList(); for (BrooklynTypePlanTransformer t: transformers) { try { - Object result = t.create(type, constraint); + T result = fn.apply(t); if (result==null) { transformersWhoDontSupport.add(t.getFormatCode() + " (returned null)"); continue; diff --git a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java index a2dc398892..acd2a8777e 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/flags/TypeCoercions.java @@ -70,6 +70,10 @@ private TypeCoercions() {} BrooklynInitialization.initTypeCoercionStandardAdapters(); } + public static TypeCoercer getInstance() { + return coercer; + } + public static void initStandardAdapters() { new BrooklynCommonAdaptorTypeCoercions(coercer).registerAllAdapters(); new CommonAdaptorTryCoercions(coercer).registerAllAdapters(); diff --git a/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java b/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java index e91693c8da..f80e44a82a 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/task/ValueResolver.java @@ -39,6 +39,7 @@ import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; import org.apache.brooklyn.util.repeat.Repeater; import org.apache.brooklyn.util.time.CountdownTimer; import org.apache.brooklyn.util.time.Duration; @@ -606,4 +607,24 @@ protected Class getType() { public String toString() { return JavaClassNames.cleanSimpleClassName(this)+"["+JavaClassNames.cleanSimpleClassName(type)+" "+value+"]"; } + + /** Returns a quick resolving type coercer. May allow more underlying {@link ValueResolver} customization in the future. */ + public static class ResolvingTypeCoercer implements TypeCoercer { + @Override + public T coerce(Object input, Class type) { + return tryCoerce(input, type).get(); + } + + @Override + public Maybe tryCoerce(Object input, Class type) { + return new ValueResolver(input, type).timeout(REAL_QUICK_WAIT).getMaybe(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Maybe tryCoerce(Object input, TypeToken type) { + return (Maybe) tryCoerce(input, type.getRawType()); + } + } + } diff --git a/core/src/main/java/org/apache/brooklyn/util/core/text/TemplateProcessor.java b/core/src/main/java/org/apache/brooklyn/util/core/text/TemplateProcessor.java index c3b8246bbf..9a71a684ba 100644 --- a/core/src/main/java/org/apache/brooklyn/util/core/text/TemplateProcessor.java +++ b/core/src/main/java/org/apache/brooklyn/util/core/text/TemplateProcessor.java @@ -20,11 +20,9 @@ import static com.google.common.base.Preconditions.checkNotNull; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; +import java.io.StringWriter; import java.util.Map; import org.apache.brooklyn.api.entity.Entity; @@ -51,7 +49,7 @@ import freemarker.cache.StringTemplateLoader; import freemarker.template.Configuration; -import freemarker.template.ObjectWrapper; +import freemarker.template.DefaultObjectWrapperBuilder; import freemarker.template.Template; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateModel; @@ -71,7 +69,7 @@ public class TemplateProcessor { protected static TemplateModel wrapAsTemplateModel(Object o) throws TemplateModelException { if (o instanceof Map) return new DotSplittingTemplateModel((Map)o); - return ObjectWrapper.DEFAULT_WRAPPER.wrap(o); + return new DefaultObjectWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build().wrap(o); } /** As per {@link #processTemplateContents(String, Map)}, but taking a file. */ @@ -503,19 +501,21 @@ public static String processTemplateContents(String templateContents, final Map< /** Processes template contents against the given {@link TemplateHashModel}. */ public static String processTemplateContents(String templateContents, final TemplateHashModel substitutions) { try { - Configuration cfg = new Configuration(); + Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); + // TODO is there a locale which doesn't require ?c everywhere??? + // seems not, and looking at DecimalFormatSymbols, creating such a locale would be ugly +// cfg.setLocale(...); StringTemplateLoader templateLoader = new StringTemplateLoader(); templateLoader.putTemplate("config", templateContents); cfg.setTemplateLoader(templateLoader); Template template = cfg.getTemplate("config"); // TODO could expose CAMP '$brooklyn:' style dsl, based on template.createProcessingEnvironment - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Writer out = new OutputStreamWriter(baos); + StringWriter out = new StringWriter(); template.process(substitutions, out); out.flush(); - return new String(baos.toByteArray()); + return out.toString(); } catch (Exception e) { log.warn("Error processing template (propagating): "+e, e); log.debug("Template which could not be parsed (causing "+e+") is:" diff --git a/core/src/main/java/org/apache/brooklyn/util/core/yoml/YomlConfigBagConstructor.java b/core/src/main/java/org/apache/brooklyn/util/core/yoml/YomlConfigBagConstructor.java new file mode 100644 index 0000000000..b960c6d72d --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/util/core/yoml/YomlConfigBagConstructor.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.core.yoml; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; + +/** + * Indicates that a class should be yoml-serialized using a one-arg constructor taking a map or bag of config. + * Similar to {@link YomlConfigMapConstructor} but accepting config-bag constructors + * and defaulting to `brooklyn.config` as the key for unknown config. + *

+ * See {@link YomlConfigMapConstructor} for the meaning of all methods. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlConfigBagConstructor { + String value(); + String writeAsKey() default "brooklyn.config"; + boolean validateAheadOfTime() default true; + boolean requireStaticKeys() default false; +} diff --git a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/StaticTypePlanTransformer.java b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/StaticTypePlanTransformer.java index a1bfa9804d..ae99312cee 100644 --- a/core/src/test/java/org/apache/brooklyn/core/catalog/internal/StaticTypePlanTransformer.java +++ b/core/src/test/java/org/apache/brooklyn/core/catalog/internal/StaticTypePlanTransformer.java @@ -27,7 +27,9 @@ import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; import org.apache.brooklyn.core.typereg.AbstractTypePlanTransformer; import org.apache.brooklyn.core.typereg.JavaClassNameTypePlanTransformer; +import org.apache.brooklyn.core.typereg.RegisteredTypeInfo; import org.apache.brooklyn.core.typereg.TypePlanTransformers; +import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.text.Identifiers; /** @@ -78,15 +80,8 @@ public static String registerSpec(AbstractBrooklynObjectSpec spec) { } @Override - public double scoreForTypeDefinition(String formatCode, Object catalogData) { - // not supported - return 0; - } - - @Override - public List createFromTypeDefinition(String formatCode, Object catalogData) { - // not supported - return null; + public RegisteredTypeInfo getTypeInfo(RegisteredType type) { + return RegisteredTypeInfo.create(type, this, null, MutableSet.of()); } @Override diff --git a/core/src/test/java/org/apache/brooklyn/core/typereg/ExampleXmlTypePlanTransformer.java b/core/src/test/java/org/apache/brooklyn/core/typereg/ExampleXmlTypePlanTransformer.java index 76570c8c10..065e917ca3 100644 --- a/core/src/test/java/org/apache/brooklyn/core/typereg/ExampleXmlTypePlanTransformer.java +++ b/core/src/test/java/org/apache/brooklyn/core/typereg/ExampleXmlTypePlanTransformer.java @@ -19,7 +19,6 @@ package org.apache.brooklyn.core.typereg; import java.io.StringReader; -import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -31,6 +30,7 @@ import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; import org.apache.brooklyn.entity.stock.BasicApplication; import org.apache.brooklyn.entity.stock.BasicEntity; +import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.core.xstream.XmlSerializer; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.stream.ReaderInputStream; @@ -80,17 +80,9 @@ protected Object createBean(RegisteredType type, RegisteredTypeLoadingContext co return new XmlSerializer().fromString((String)type.getPlan().getPlanData()); } - - @Override - public double scoreForTypeDefinition(String formatCode, Object catalogData) { - // defining types not supported - return 0; - } - @Override - public List createFromTypeDefinition(String formatCode, Object catalogData) { - // defining types not supported - return null; + public RegisteredTypeInfo getTypeInfo(RegisteredType type) { + return RegisteredTypeInfo.create(type, this, null, MutableSet.of()); } private Document parseXml(String plan) { diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java index 21046add67..7453edb2ff 100644 --- a/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java +++ b/software/base/src/main/java/org/apache/brooklyn/entity/java/JmxAttributeSensor.java @@ -88,7 +88,7 @@ public JmxAttributeSensor(final ConfigBag params) { public void apply(final EntityLocal entity) { super.apply(entity); - final Boolean suppressDuplicates = EntityInitializers.resolve(params, SUPPRESS_DUPLICATES); + final Boolean suppressDuplicates = EntityInitializers.resolve(getRememberedParams(), SUPPRESS_DUPLICATES); if (entity instanceof UsesJmx) { if (LOG.isDebugEnabled()) { diff --git a/software/winrm/src/main/java/org/apache/brooklyn/core/sensor/windows/WinRmCommandSensor.java b/software/winrm/src/main/java/org/apache/brooklyn/core/sensor/windows/WinRmCommandSensor.java index 2fcc31502e..2a5e565ff8 100644 --- a/software/winrm/src/main/java/org/apache/brooklyn/core/sensor/windows/WinRmCommandSensor.java +++ b/software/winrm/src/main/java/org/apache/brooklyn/core/sensor/windows/WinRmCommandSensor.java @@ -96,7 +96,7 @@ public void apply(final EntityLocal entity) { LOG.debug("Adding WinRM sensor {} to {}", name, entity); } - final Boolean suppressDuplicates = EntityInitializers.resolve(params, SUPPRESS_DUPLICATES); + final Boolean suppressDuplicates = EntityInitializers.resolve(getRememberedParams(), SUPPRESS_DUPLICATES); Supplier> envSupplier = new Supplier>() { @Override diff --git a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java index 6975139f68..b8c99ac8d7 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java +++ b/utils/common/src/main/java/org/apache/brooklyn/test/Asserts.java @@ -757,6 +757,9 @@ public static AssertionError fail(String message) { public static AssertionError fail(Throwable error) { throw new AssertionError(error); } + public static AssertionError fail(String message, Throwable throwable) { + throw new AssertionError(message, throwable); + } public static AssertionError fail() { throw new AssertionError(); } public static void assertEqualsIgnoringOrder(Iterable actual, Iterable expected) { diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java index c7e1742cc8..1b695a0f92 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java @@ -227,8 +227,8 @@ public void recordVisit(Object o) { protected boolean isTrivial(Object o) { if (o == null) return true; - if (o instanceof Map && ((Map)o).isEmpty()) return true; - if (o instanceof Iterable && Iterables.isEmpty(((Iterable)o))) return true; + if (o instanceof Map && ((Map)o).isEmpty()) return true; + if (o instanceof Iterable && Iterables.isEmpty(((Iterable)o))) return true; Class clazz = o.getClass(); return clazz.isEnum() || clazz.isPrimitive() || TRIVIAL_CLASSES.contains(clazz); } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableList.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableList.java index 6383a09acd..3d100ed366 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableList.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableList.java @@ -48,7 +48,7 @@ public static MutableList of(V v1) { return result; } - public static MutableList of(V v1, V v2, V ...vv) { + public static MutableList of(V v1, V v2, @SuppressWarnings("unchecked") V ...vv) { MutableList result = new MutableList(); result.add(v1); result.add(v2); @@ -125,7 +125,7 @@ public Builder add(V value) { return this; } - public Builder add(V value1, V value2, V ...values) { + public Builder add(V value1, V value2, @SuppressWarnings("unchecked") V ...values) { result.add(value1); result.add(value2); for (V v: values) result.add(v); @@ -191,7 +191,7 @@ public ImmutableList buildImmutable() { return ImmutableList.copyOf(result); } - public Builder addLists(Iterable ...items) { + public Builder addLists(@SuppressWarnings("unchecked") Iterable ...items) { for (Iterable item: items) { addAll(item); } @@ -212,7 +212,7 @@ public MutableList appendIfNotNull(V item) { } /** as {@link List#add(Object)} but accepting multiple, and fluent style */ - public MutableList append(V item1, V item2, V ...items) { + public MutableList append(V item1, V item2, @SuppressWarnings("unchecked") V ...items) { add(item1); add(item2); for (V item: items) add(item); @@ -220,7 +220,7 @@ public MutableList append(V item1, V item2, V ...items) { } /** as {@link List#add(Object)} but excluding nulls, accepting multiple, and fluent style */ - public MutableList appendIfNotNull(V item1, V item2, V ...items) { + public MutableList appendIfNotNull(V item1, V item2, @SuppressWarnings("unchecked") V ...items) { if (item1!=null) add(item1); if (item2!=null) add(item2); for (V item: items) diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableMap.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableMap.java index 41a9bd12b9..5bfeb24cc1 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableMap.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/MutableMap.java @@ -193,11 +193,25 @@ public Builder put(Entry entry) { return this; } + public Builder putIfAbsent(Entry entry) { + if (!result.containsKey(entry.getKey())) result.put(entry.getKey(), entry.getValue()); + return this; + } + public Builder putAll(Map map) { result.add(map); return this; } + public Builder putAllAbsent(Map map) { + if (map!=null) { + for (Map.Entry entry: map.entrySet()) { + putIfAbsent(entry); + } + } + return this; + } + public Builder remove(K key) { result.remove(key); return this; diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java index 33952964e7..6006b74517 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/ReflectionPredicates.java @@ -30,23 +30,28 @@ public class ReflectionPredicates { public static Predicate MODIFIERS_PRIVATE = new ModifiersPrivate(); private static class ModifiersPrivate implements Predicate { @Override public boolean apply(Integer modifiers) { return Modifier.isPrivate(modifiers); } + @Override public String toString() { return "private"; } } public static Predicate MODIFIERS_PUBLIC = new ModifiersPublic(); private static class ModifiersPublic implements Predicate { @Override public boolean apply(Integer modifiers) { return Modifier.isPublic(modifiers); } + @Override public String toString() { return "public"; } } public static Predicate MODIFIERS_PROTECTED = new ModifiersProtected(); private static class ModifiersProtected implements Predicate { @Override public boolean apply(Integer modifiers) { return Modifier.isProtected(modifiers); } + @Override public String toString() { return "protected"; } } public static Predicate MODIFIERS_TRANSIENT = new ModifiersTransient(); private static class ModifiersTransient implements Predicate { @Override public boolean apply(Integer modifiers) { return Modifier.isTransient(modifiers); } + @Override public String toString() { return "transient"; } } public static Predicate MODIFIERS_STATIC = new ModifiersStatic(); private static class ModifiersStatic implements Predicate { @Override public boolean apply(Integer modifiers) { return Modifier.isStatic(modifiers); } + @Override public String toString() { return "static"; } } public static Predicate fieldModifiers(Predicate modifiersCheck) { return new FieldModifiers(modifiersCheck); } @@ -54,6 +59,7 @@ private static class FieldModifiers implements Predicate { private Predicate modifiersCheck; private FieldModifiers(Predicate modifiersCheck) { this.modifiersCheck = modifiersCheck; } @Override public boolean apply(Field f) { return modifiersCheck.apply(f.getModifiers()); } + @Override public String toString() { return "modifiers["+modifiersCheck+"]"; } } public static Predicate IS_FIELD_PUBLIC = fieldModifiers(MODIFIERS_PUBLIC); public static Predicate IS_FIELD_TRANSIENT = fieldModifiers(MODIFIERS_TRANSIENT); diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java index be6e9a4da0..79dd162e7c 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/Reflections.java @@ -339,7 +339,9 @@ public static Maybe invokeConstructorFromArgs(Reflections reflections, Cl return Maybe.of((T) reflections.loadInstance(constructor, argsArray)); } } - return Maybe.absent("Constructor not found"); + if (argsArray==null || argsArray.length==0) + return Maybe.absent("No no-arg constructor availble for "+clazz); + return Maybe.absent("No matching constructor availble for "+clazz+", parameters "+Arrays.asList(argsArray)); } @@ -648,6 +650,42 @@ public static Method findMethod(Class clazz, String name, Class... paramet throw toThrowIfFails; } + /** Combination of {@link Class#getConstructors()} and {@link Class#getDeclaredConstructors()} returning a list with correct generics */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static List> getConstructors(Class clazz) { + return (List) MutableList.copyOf(Arrays.asList(clazz.getConstructors())).appendAll(Arrays.asList(clazz.getDeclaredConstructors())); + } + /** Returns any constructor exactly matching the given signature, including privates and on parent classes. */ + public static Maybe> findConstructorExactMaybe(Class clazz, Class... parameterTypes) { + if (clazz == null) return Maybe.absentNoTrace("class is null"); + Iterable> result = findConstructors(false, clazz, parameterTypes); + if (!result.iterator().hasNext()) return Maybe.absentNoTrace("no Constructors matching "+clazz.getName()+"("+Arrays.asList(parameterTypes)+")"); + return Maybe.of(result.iterator().next()); + } + /** Returns all constructors compatible with the given argument types, including privates and on parent classes and where the Constructor takes a supertype. */ + public static Iterable> findConstructorsCompatible(Class clazz, Class... parameterTypes) { + return findConstructors(true, clazz, parameterTypes); + } + private static Iterable> findConstructors(boolean allowCovariantParameterClasses, Class clazz, Class... parameterTypes) { + if (clazz == null) { + return Collections.emptySet(); + } + List> result = MutableList.of(); + + constructors: for (Constructor m: getConstructors(clazz)) { + if (m.getParameterTypes().length!=parameterTypes.length) continue constructors; + parameters: for (int i=0; i @@ -739,7 +777,11 @@ public static List findFields(final Class clazz, Predicate filt if (!visited.add(nextclazz)) { continue; // already visited } + if (nextclazz.getSuperclass() != null) tovisit.add(nextclazz.getSuperclass()); + + // interfaces are only necessary for statics ... but the caller might be interested in that + // (could have a new method which returns non-statics only to optimize this) tovisit.addAll(Arrays.asList(nextclazz.getInterfaces())); result.addAll(Iterables.filter(Arrays.asList(nextclazz.getDeclaredFields()), diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java index 0b2a63681a..a55f994f0e 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/javalang/coerce/PrimitiveStringTypeCoercions.java @@ -31,7 +31,7 @@ public class PrimitiveStringTypeCoercions { public PrimitiveStringTypeCoercions() {} - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({ "unchecked" }) public static Maybe tryCoerce(Object value, Class targetType) { //deal with primitive->primitive casting if (isPrimitiveOrBoxer(targetType) && isPrimitiveOrBoxer(value.getClass())) { diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java b/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java index 8cf34e296b..6ee3046bdb 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/time/Duration.java @@ -29,12 +29,18 @@ import javax.annotation.Nullable; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlAsPrimitive; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; /** simple class determines a length of time */ +@YomlAllFieldsTopLevel +@YomlAsPrimitive +@Alias("duration") public class Duration implements Comparable, Serializable { private static final long serialVersionUID = -2303909964519279617L; @@ -53,9 +59,13 @@ public class Duration implements Comparable, Serializable { /** longest supported duration, 2^{63}-1 nanoseconds, approx ten billion seconds, or 300 years */ public static final Duration PRACTICALLY_FOREVER = of(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + public static final String PRACTICALLY_FOREVER_NAME = "a very long time"; private final long nanos; + @SuppressWarnings("unused") // for yoml creation + private Duration() { nanos=-1; } + public Duration(long value, TimeUnit unit) { if (value != 0) { Preconditions.checkNotNull(unit, "Cannot accept null timeunit (unless value is 0)"); @@ -72,6 +82,7 @@ public int compareTo(Duration o) { @Override public String toString() { + if (nanos==PRACTICALLY_FOREVER.nanos) return PRACTICALLY_FOREVER_NAME; return Time.makeTimeStringExact(this); } @@ -145,9 +156,13 @@ public static Duration parse(String textualDescription) { if (Strings.isBlank(textualDescription)) return null; if ("null".equalsIgnoreCase(textualDescription)) return null; - if ("forever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; - if ("practicallyforever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; - if ("practically_forever".equalsIgnoreCase(textualDescription)) return Duration.PRACTICALLY_FOREVER; + if (!textualDescription.matches(".*[0-9].*")) { + // look for text matches if there are no numbers in it + String t = textualDescription.toLowerCase(); + if (PRACTICALLY_FOREVER_NAME.equals(t)) return Duration.PRACTICALLY_FOREVER; + if (t.matches("(practically[-_\\s]*)?forever")) return Duration.PRACTICALLY_FOREVER; + if (t.matches("(a[-_\\s]+)?(very[-_,\\s]*)*(long[-_,\\s]*)+(time)?")) return Duration.PRACTICALLY_FOREVER; + } return new Duration((long) Time.parseElapsedTimeAsDouble(textualDescription), TimeUnit.MILLISECONDS); } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/Yoml.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/Yoml.java new file mode 100644 index 0000000000..f0c97c22b0 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/Yoml.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlConverter; + + +public class Yoml { + + final YomlConfig config; + + private Yoml(YomlConfig config) { this.config = config; } + + public static Yoml newInstance(YomlConfig config) { + return new Yoml(config); + } + + public static Yoml newInstance(YomlTypeRegistry typeRegistry) { + return new Yoml(YomlConfig.Builder.builder().typeRegistry(typeRegistry).serializersPostAddDefaults().build()); + } + + public YomlConfig getConfig() { + return config; + } + + public Object read(String yaml) { + return read(yaml, null); + } + public Object read(String yaml, String expectedType) { + return readFromYamlObject(new org.yaml.snakeyaml.Yaml().load(yaml), expectedType); + } + public Object readFromYamlObject(Object yamlObject, String type) { + return new YomlConverter(config).read( new YomlContextForRead(yamlObject, "", type, null) ); + } + + public Object write(Object java) { + return write(java, null); + } + public Object write(Object java, String expectedType) { + return new YomlConverter(config).write( new YomlContextForWrite(java, "", expectedType, null) ); + } + +// public T read(String yaml, Class type) { +// } +// public T read(String yaml, TypeToken type) { +// } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlConfig.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlConfig.java new file mode 100644 index 0000000000..9d39c892bd --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlConfig.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import java.util.List; + +import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; +import org.apache.brooklyn.util.yoml.internal.YomlConfigs; + +public interface YomlConfig { + + public YomlTypeRegistry getTypeRegistry(); + public TypeCoercer getCoercer(); + public List getSerializersPost(); + + public static class Builder extends YomlConfigs.Builder { + public static Builder builder() { return new Builder(); } + public static Builder builder(YomlConfig source) { return new Builder(source); } + + protected Builder() { } + protected Builder(YomlConfig source) { super(source); } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlException.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlException.java new file mode 100644 index 0000000000..c282521b7e --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlException.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +public class YomlException extends RuntimeException { + + private static final long serialVersionUID = 7825908737102292499L; + + YomlContext context; + + public YomlException(String message) { super(message); } + public YomlException(String message, Throwable cause) { super(message, cause); } + public YomlException(String message, YomlContext context) { this(message); this.context = context; } + public YomlException(String message, YomlContext context, Throwable cause) { this(message, cause); this.context = context; } + + public YomlContext getContext() { + return context; + } + + @Override + public String getMessage() { + if (context==null) return getBaseMessage(); + return getBaseMessage() + " ("+context+")"; + } + + public String getBaseMessage() { + return super.getMessage(); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlRequirement.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlRequirement.java new file mode 100644 index 0000000000..cae4b69e2e --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlRequirement.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +public interface YomlRequirement { + + void checkCompletion(YomlContext context); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlSerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlSerializer.java new file mode 100644 index 0000000000..c6392d544f --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlSerializer.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlConverter; +import org.apache.brooklyn.util.yoml.serializers.YomlSerializerComposition; + +/** Describes a serializer which can be used by {@link YomlConverter}. + *

+ * Instances of this class should be thread-safe for use with simultaneous conversions. + * Often implementations will extend {@link YomlSerializerComposition} and which stores + * per-conversion data in a per-method-invocation object. + */ +public interface YomlSerializer { + + /** + * modifies yaml object and/or java object and/or blackboard as appropriate, + * when trying to build a java object from a yaml object, + * returning true if it did anything (and so should restart the cycle). + * implementations must NOT return true indefinitely if passed the same instances! + */ + public void read(YomlContextForRead context, YomlConverter converter); + + /** + * modifies java object and/or yaml object and/or blackboard as appropriate, + * when trying to build a yaml object from a java object, + * returning true if it did anything (and so should restart the cycle). + * implementations must NOT return true indefinitely if passed the same instances! + */ + public void write(YomlContextForWrite context, YomlConverter converter); + + /** + * generates human-readable schema for a type using this schema. + */ + public String document(String type, YomlConverter converter); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlTypeRegistry.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlTypeRegistry.java new file mode 100644 index 0000000000..b753bcd959 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/YomlTypeRegistry.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +public interface YomlTypeRegistry { + + Object newInstance(String type, Yoml yoml); + + /** Absent if unknown type; throws if type is ill-defined or incomplete. */ + Maybe newInstanceMaybe(String type, Yoml yomlToUseForSubsequentEvaluation); + + /** As {@link #newInstance(String, Yoml)} but for use when YOML is making a nested call, + * in case different resolution strategies apply inside the hierarchy. */ + Maybe newInstanceMaybe(String type, Yoml yomlToUseForSubsequentEvaluation, @Nullable YomlContext yomlContextOfThisCall); + + /** Returns the most-specific Java type implied by the given type in the registry, + * or a maybe wrapping any explanatory error if the type is not available in the registry. + *

+ * This is needed so that the right deserialization strategies can be applied for + * things like collections and enums. + */ + Maybe> getJavaTypeMaybe(@Nullable String typeName, @Nullable YomlContext yomlContextOfThisCall); + + /** Return the best known type name to describe the given java instance */ + String getTypeName(Object obj); + /** Return the type name to describe the given java class */ + String getTypeNameOfClass(Class type); + + /** Return custom serializers that shoud be used when deserializing something of the given type, + * typically also looking at serializers for its supertypes */ + Iterable getSerializersForType(String typeName, @Nullable YomlContext yomlContextOfThisCall); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/Alias.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/Alias.java new file mode 100644 index 0000000000..6532ebd355 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/Alias.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ TYPE, FIELD }) +@Inherited +/** Indicates that a class or field should be known by a set of given aliases */ +public @interface Alias { + + String[] value() default {}; + + /** Indicates an alias preferred over the name in code, e.g. when serializing an instance. + * This will be added to any list in {@link #value()} so there is no need to declare a preferred alias in both places. */ + String preferred() default ""; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/DefaultKeyValue.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/DefaultKeyValue.java new file mode 100644 index 0000000000..ee990bef84 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/DefaultKeyValue.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +/** Indicates that default key-value pair should be supplied, e.g. for YomlSingletonMap */ +public @interface DefaultKeyValue { + + String key(); + String val(); + + /** Whether the value should be treated as a string, set directly as the value (the default), + * or whether it should be parsed as YAML and the resulting JSON map/list/primitive used as the value. */ + boolean valNeedsParsing() default false; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAllFieldsTopLevel.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAllFieldsTopLevel.java new file mode 100644 index 0000000000..86f3cf05c3 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAllFieldsTopLevel.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +/** Indicates that all fields should be available at the top-level when reading yoml, + * ie none require to be inside a fields block. */ +public @interface YomlAllFieldsTopLevel { +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAnnotations.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAnnotations.java new file mode 100644 index 0000000000..fa82dc6c9f --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAnnotations.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultValue; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.serializers.ConvertFromPrimitive; +import org.apache.brooklyn.util.yoml.serializers.ConvertSingletonMap; +import org.apache.brooklyn.util.yoml.serializers.DefaultMapValuesSerializer; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistryUsingConfigMap; +import org.apache.brooklyn.util.yoml.serializers.RenameKeySerializer; +import org.apache.brooklyn.util.yoml.serializers.RenameKeySerializer.RenameDefaultKey; +import org.apache.brooklyn.util.yoml.serializers.RenameKeySerializer.RenameDefaultValue; +import org.apache.brooklyn.util.yoml.serializers.TopLevelFieldSerializer; +import org.apache.brooklyn.util.yoml.serializers.TypeFromOtherFieldSerializer; + +public class YomlAnnotations { + + public Set findTypeNamesFromAnnotations(Class type, String optionalDefaultPreferredTypeName, boolean includeJavaTypeNameEvenIfOthers) { + MutableSet names = MutableSet.of(); + + Alias overallAlias = type.getAnnotation(Alias.class); + if (optionalDefaultPreferredTypeName!=null) { + names.addIfNotNull(optionalDefaultPreferredTypeName); + } + if (overallAlias!=null) { + if (Strings.isNonBlank(overallAlias.preferred())) { + names.add( overallAlias.preferred() ); + } + names.addAll( Arrays.asList(overallAlias.value()) ); + } + if (includeJavaTypeNameEvenIfOthers || names.isEmpty()) { + names.add(type.getName()); + } + + return names; + } + + public Collection findTopLevelFieldSerializers(Class t, boolean requireAnnotation) { + List result = MutableList.of(); + Map fields = YomlUtils.getAllNonTransientNonStaticFields(t, null); + for (Map.Entry f: fields.entrySet()) { + if (!requireAnnotation || f.getValue().isAnnotationPresent(YomlTopLevelField.class)) { + result.add(new TopLevelFieldSerializer(f.getKey(), f.getValue())); + YomlTypeFromOtherField typeFromOther = f.getValue().getAnnotation(YomlTypeFromOtherField.class); + if (typeFromOther!=null) { + result.add(new TypeFromOtherFieldSerializer(f.getKey(), typeFromOther)); + } + } + } + return result; + } + + public Collection findConfigMapConstructorSerializersIgnoringConfigInheritance(Class t) { + YomlConfigMapConstructor ann = t.getAnnotation(YomlConfigMapConstructor.class); + if (ann==null) return Collections.emptyList(); + return InstantiateTypeFromRegistryUsingConfigMap.newFactoryIgnoringInheritance().newConfigKeySerializersForType( + t, + ann.value(), Strings.isNonBlank(ann.writeAsKey()) ? ann.writeAsKey() : ann.value(), + ann.validateAheadOfTime(), ann.requireStaticKeys()); + } + + public Collection findSingletonMapSerializers(Class t) { + YomlSingletonMap ann = t.getAnnotation(YomlSingletonMap.class); + if (ann==null) return Collections.emptyList(); + return MutableList.of((YomlSerializer) new ConvertSingletonMap(ann)); + } + + public Collection findConvertFromPrimitiveSerializers(Class t) { + YomlFromPrimitive ann = t.getAnnotation(YomlFromPrimitive.class); + if (ann==null) return Collections.emptyList(); + return MutableList.of((YomlSerializer) new ConvertFromPrimitive(ann)); + } + + public Collection findDefaultMapValuesSerializers(Class t) { + YomlDefaultMapValues ann = t.getAnnotation(YomlDefaultMapValues.class); + if (ann==null) return Collections.emptyList(); + return MutableList.of((YomlSerializer) new DefaultMapValuesSerializer(ann)); + } + + public Collection findRenameKeySerializers(Class t) { + MutableList result = MutableList.of(); + YomlRenameKey ann1 = t.getAnnotation(YomlRenameKey.class); + if (ann1!=null) result.add(new RenameKeySerializer(ann1)); + YomlRenameDefaultKey ann2 = t.getAnnotation(YomlRenameDefaultKey.class); + if (ann2!=null) result.add(new RenameDefaultKey(ann2)); + YomlRenameDefaultValue ann3 = t.getAnnotation(YomlRenameDefaultValue.class); + if (ann3!=null) result.add(new RenameDefaultValue(ann3)); + return result; + } + + /** Adds the default set of serializer annotations */ + public Set findSerializerAnnotations(Class type, boolean recurseUpIfEmpty) { + Set result = MutableSet.of(); + if (type==null) return result; + + collectSerializerAnnotationsAtClass(result, type); + boolean canRecurse = result.isEmpty(); + + if (recurseUpIfEmpty && canRecurse) { + result.addAll(findSerializerAnnotations(type.getSuperclass(), recurseUpIfEmpty)); + } + return result; + } + + protected void collectSerializerAnnotationsAtClass(Set result, Class type) { + collectSerializersLowLevel(result, type); + collectSerializersForConfig(result, type); + collectSerializersFields(result, type); + // subclasses can extend or override the methods above + } + + protected void collectSerializersFields(Set result, Class type) { + YomlAllFieldsTopLevel allFields = type.getAnnotation(YomlAllFieldsTopLevel.class); + result.addAll(findTopLevelFieldSerializers(type, allFields==null)); + } + + protected void collectSerializersForConfig(Set result, Class type) { + result.addAll(findConfigMapConstructorSerializersIgnoringConfigInheritance(type)); + } + + protected void collectSerializersLowLevel(Set result, Class type) { + result.addAll(findConvertFromPrimitiveSerializers(type)); + result.addAll(findRenameKeySerializers(type)); + result.addAll(findSingletonMapSerializers(type)); + result.addAll(findDefaultMapValuesSerializers(type)); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAsPrimitive.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAsPrimitive.java new file mode 100644 index 0000000000..8baf9f7b01 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlAsPrimitive.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.util.yoml.serializers.ConvertFromPrimitive; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypePrimitive; + +/** + * Indicates that a class is potentially coercible to and from a primitive. + * YOML will always try to coerce from a primitive if appropriate, + * but this indicates that it should try various strategies to coerce to a primitive. + *

+ * See {@link InstantiateTypePrimitive}. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +public @interface YomlAsPrimitive { + + /** The key to insert for the given value */ + String keyToInsert() default ConvertFromPrimitive.DEFAULT_DEFAULT_KEY; + + DefaultKeyValue[] defaults() default {}; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlConfigMapConstructor.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlConfigMapConstructor.java new file mode 100644 index 0000000000..0a44e9895b --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlConfigMapConstructor.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.config.ConfigKey; + +/** + * Indicates that a class should be yoml-serialized using a one-arg constructor taking a map of config. + * Types will be inferred where possible based on the presence of {@link ConfigKey} static fields in the type. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlConfigMapConstructor { + /** YOML needs to know which field contains the config at serialization time. + *

+ * It can be supplied as blank to mean that a map should be taken as a constructor + * but data will be written to fields. In that case the output YAML serialization will refer to + * the fields but config keys will be accepted for input. */ + String value(); + /** By default YOML reads/writes unrecognised key values against a key with the same name as {@link #value()}. + * This can be set to use a different key in the YAML. */ + String writeAsKey() default ""; + + /** Validate that a suitable field and constructor exist, failing fast if not */ + boolean validateAheadOfTime() default true; + + /** Skip if there are no declared config keys (default false) */ + boolean requireStaticKeys() default false; +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlDefaultMapValues.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlDefaultMapValues.java new file mode 100644 index 0000000000..f0f746b7aa --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlDefaultMapValues.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates that default values should be placed into a map if not present. + *

+ * Presence is tested individually on keys. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlDefaultMapValues { + + DefaultKeyValue[] value(); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlFromPrimitive.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlFromPrimitive.java new file mode 100644 index 0000000000..2d627bc7f0 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlFromPrimitive.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.util.yoml.serializers.ConvertFromPrimitive; + +/** + * Indicates that a class can be yoml-serialized as a primitive or list + * reflecting a single field in the object which will take the primitive value. + *

+ * If no {@link #keyToInsert()} is supplied the value is set under the key + * .value for use by other serializers. + *

+ * See {@link ConvertFromPrimitive}. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlFromPrimitive { + + /** The key to insert for the given value */ + String keyToInsert() default ConvertFromPrimitive.DEFAULT_DEFAULT_KEY; + + DefaultKeyValue[] defaults() default {}; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlRenameKey.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlRenameKey.java new file mode 100644 index 0000000000..1a9c128533 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlRenameKey.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.util.yoml.serializers.RenameKeySerializer; + +/** + * Indicates that a key should be renamed when reading yaml. + *

+ * See {@link RenameKeySerializer}. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlRenameKey { + + /** The key name to change from when reading */ + String oldKeyName(); + + /** The key name to change to when reading */ + String newKeyName(); + + DefaultKeyValue[] defaults() default {}; + + /** As {@link YomlRenameKey} with {@link YomlRenameKey#oldKeyName()} equals to .key */ + @Retention(RUNTIME) + @Target({ TYPE }) + @Inherited + public @interface YomlRenameDefaultKey { + /** The key name to change to when reading */ + String value(); + DefaultKeyValue[] defaults() default {}; + } + + /** As {@link YomlRenameKey} with {@link YomlRenameKey#oldKeyName()} equals to .value */ + @Retention(RUNTIME) + @Target({ TYPE }) + @Inherited + public @interface YomlRenameDefaultValue { + /** The key name to change to when reading */ + String value(); + DefaultKeyValue[] defaults() default {}; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlSingletonMap.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlSingletonMap.java new file mode 100644 index 0000000000..a505f860e2 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlSingletonMap.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apache.brooklyn.util.yoml.serializers.ConvertSingletonMap; + +/** + * Indicates that a class can be yoml-serialized as a map with a single key, + * where the single-key map is converted to a bigger map such that + * the value of that single key is set under as one key ({@link #keyForKey()}), + * and the value either merged (if a map and {@link #keyForMapValue()} is blank) + * or placed under a different key ({@link #keyForAnyValue()} and other value keys). + *

+ * Default values (.key and .value) are intended for + * use by other serializers. + *

+ * See {@link ConvertSingletonMap}. + */ +@Retention(RUNTIME) +@Target({ TYPE }) +@Inherited +public @interface YomlSingletonMap { + /** The single key is taken as a value against the key name given here. */ + String keyForKey() default ConvertSingletonMap.DEFAULT_KEY_FOR_KEY; + + /** If value is a primitive or string, place under a key, + * the name of which is given here. */ + String keyForPrimitiveValue() default ""; + /** If value is a list, place under a key, + * the name of which is given here. */ + String keyForListValue() default ""; + /** If value is a map, place under a key, + * the name of which is given here. */ + String keyForMapValue() default ""; + /** Any value (including a map) is placed in a different key, + * the name of which is given here, + * but after more specific types are applied. */ + String keyForAnyValue() default ConvertSingletonMap.DEFAULT_KEY_FOR_VALUE; + + DefaultKeyValue[] defaults() default {}; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTopLevelField.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTopLevelField.java new file mode 100644 index 0000000000..558103c74c --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTopLevelField.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target({ FIELD }) +/** Indicates that this field should be settable at the top-level when reading yoml */ +public @interface YomlTopLevelField { + + // could allow configuration for the TopLevelField representing the field this annotates + // (that constructor looks at this annotation) + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTypeFromOtherField.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTypeFromOtherField.java new file mode 100644 index 0000000000..4a8ac35ffa --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/annotations/YomlTypeFromOtherField.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Indicates that the type of a field should be taken at runtime from another field. + * This is useful to allow a user to specify e.g. { objType: int, obj: 3 } + * rather than requiring { obj: { type: int, value: e } }. + *

+ * By default the other field is assumed to exist but that need not be the case. + */ +@Retention(RUNTIME) +@Target(FIELD) +public @interface YomlTypeFromOtherField { + + /** The other field which will supply the type. This must point at the field name or the config key name, + * not any alias, although aliases can be used in the YAML when specifying the type. */ + String value(); + + /** Whether the other field is real in the java object, + * ie present as a field or config key, and so should be serialized/deserialized normally; + * if false it will be created on writing to yaml and deleted while reading, + * ie not reflected in the java object */ + boolean real() default true; + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstruction.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstruction.java new file mode 100644 index 0000000000..8ed5bd5924 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstruction.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.util.List; + +import org.apache.brooklyn.util.guava.Maybe; + +/** Capture instructions on how an object should be created. + *

+ * This is used when we need information from an outer instance definition in order to construct the object. + * (The default pathway is to use a no-arg constructor and to apply the outer definitions to the instance. + * But that doesn't necessarily work if a specific constructor or static method is being expected. + * It gets more complicated if the outer instance is overwriting information from an inner instance, + * but it is the inner instance which actually defines the java type to instantiate ... and that isn't so uncommon!) + */ +public interface ConstructionInstruction { + + public Class getType(); + public List getArgs(); + public Maybe create(); + + public ConstructionInstruction getOuterInstruction(); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstructions.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstructions.java new file mode 100644 index 0000000000..b25668f8e8 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/ConstructionInstructions.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; + +/** Utilities for working with {@link ConstructionInstruction} */ +public class ConstructionInstructions { + + private static final Logger log = LoggerFactory.getLogger(ConstructionInstructions.class); + + public static class Factory { + + /** Returns a new construction instruction which creates an instance of the given type, + * preferring the optional instruction but enforcing the given type constraint, + * instructing to create using a no-arg constructor if this is the outermost instruction, + * and if an inner instruction not putting any constraints on the arguments. + * see {@link #newUsingConstructorWithArgs(Class, List, ConstructionInstruction)}. */ + public static ConstructionInstruction newDefault(Class type, ConstructionInstruction optionalOuter) { + if (optionalOuter!=null) { + if (optionalOuter instanceof WrappingConstructionInstruction) { + return newUsingConstructorWithArgs(type, null, optionalOuter); + } else { + log.warn("Ignoring nested construction instruction which is not a wrapping instruction: "+optionalOuter+" for "+type); + } + } + return newUsingConstructorWithArgs(type, null, null); + } + + /** Returns a new construction instruction which creates an instance of the given type with the given args, + * preferring the optional instruction but enforcing the given type constraint and exposing the given args for it to inherit, + * and if there is no outer instruction it creates from a constructor taking the given args + * and merging with any inner instruction's args using the given strategy + * (if args are null here, the merge strategies should prefer the inner args, + * and if all args everywhere are null, using the no-arg constuctor, as per {@link #newDefault(Class, ConstructionInstruction)}) */ + public static ConstructionInstruction newUsingConstructorWithArgsAndArgsListMergeStrategy( + Class type, @Nullable List args, ConstructionInstruction optionalOuter, ObjectMergeStrategy argsListMergeStrategy) { + return new BasicConstructionWithArgsInstruction(type, args, (BasicConstructionWithArgsInstruction)optionalOuter, + argsListMergeStrategy); + } + + /** As {@link #newUsingConstructorWithArgsAndArgsListMergeStrategy(Class, List, ConstructionInstruction, ObjectMergeStrategy)} + * but using the default argument list strategy {@link ArgsListMergeStrategy} + * (which asserts arguments lists are the same size and fails if they are different, ignoring any null lists) + * configured to merge individual arguments according to the given provided strategy */ + public static ConstructionInstruction newUsingConstructorWithArgsAndArgumentMergeStrategy( + Class type, @Nullable List args, ConstructionInstruction optionalOuter, ObjectMergeStrategy argumentMergeStrategy) { + return new BasicConstructionWithArgsInstruction(type, args, (WrappingConstructionInstruction)optionalOuter, + new ArgsListMergeStrategy(argumentMergeStrategy)); + } + + /** As {@link #newUsingConstructorWithArgsAndArgumentMergeStrategy(Class, List, ConstructionInstruction, ObjectMergeStrategy)} + * but using {@link MapMergeStrategy} to merge arguments, ie: + * argument lists are required to be the same length or at least one null; + * where an argument in the list is a map they are deeply merged, and otherwise the othermost is preferred */ + public static ConstructionInstruction newUsingConstructorWithArgs( + Class type, @Nullable List args, ConstructionInstruction optionalOuter) { + return newUsingConstructorWithArgsAndArgumentMergeStrategy(type, args, optionalOuter, + MapMergeStrategy.DEEP_MERGE); + } + + } + + public interface ObjectMergeStrategy { + public Object merge(Object obj1, Object obj2); + } + + /** Merge arg lists, as follows: + * if either list is null, take the other; + * otherwise require them to be the same length. + *

+ * If any arg is not a list, this throws. The return type is guaranteed to be null, + * or a list the same size as the (at least one) non-null list argument. + *

+ * For each entry, it applies the given deep merge strategy. + */ + public static class ArgsListMergeStrategy implements ObjectMergeStrategy { + final ObjectMergeStrategy strategyForEachArgument; + + public ArgsListMergeStrategy(ObjectMergeStrategy strategyForEachArgument) { + this.strategyForEachArgument = Preconditions.checkNotNull(strategyForEachArgument); + } + + public Object merge(Object olderArgs, Object args) { + if (!(olderArgs==null || olderArgs instanceof List) || !(args==null || args instanceof List)) + throw new IllegalArgumentException("Only merges lists, not "+olderArgs+" and "+args); + return mergeLists((List)olderArgs, (List)args); + } + + public List mergeLists(List olderArgs, List args) { + if (olderArgs==null) return args; + if (args==null) return olderArgs; + + List newArgs; + if (olderArgs.size() != args.size()) + throw new IllegalStateException("Incompatible arguments, sizes "+olderArgs.size()+" and "+args.size()+": "+olderArgs+" and "+args); + // merge + newArgs = MutableList.of(); + Iterator i1 = olderArgs.iterator(); + Iterator i2 = args.iterator(); + while (i2.hasNext()) { + Object o1 = i1.next(); + Object o2 = i2.next(); + newArgs.add(strategyForEachArgument.merge(o1, o2)); + } + return newArgs; + } + } + + public static class AlwaysPreferLatter implements ObjectMergeStrategy { + @Override + public Object merge(Object obj1, Object obj2) { + return obj2; + } + } + + public static class MapMergeStrategy implements ObjectMergeStrategy { + + public static ObjectMergeStrategy DEEP_MERGE = new MapMergeStrategy(); + static { ((MapMergeStrategy)DEEP_MERGE).setStrategyForMapEntries(DEEP_MERGE); } + + public MapMergeStrategy() {} + + ObjectMergeStrategy strategyForMapEntries = new AlwaysPreferLatter(); + + public void setStrategyForMapEntries(ObjectMergeStrategy strategyForMapEntries) { + this.strategyForMapEntries = strategyForMapEntries; + } + + @Override + public Object merge(Object o1, Object o2) { + if (applies(o1, o2)) { + return mergeMapsDeep((Map)o1, (Map)o2); + } + // prefer o2 even if null, as a way of overwriting a map in its entirety + return o2; + } + + public boolean applies(Object o1, Object o2) { + return (o1 instanceof Map) && (o2 instanceof Map); + } + + public Map mergeMapsDeep(Map o1, Map o2) { + MutableMap result = MutableMap.copyOf(o1); + if (o2!=null) { + for (Map.Entry e2: o2.entrySet()) { + Object v2 = e2.getValue(); + if (result.containsKey(e2.getKey())) { + v2 = strategyForMapEntries.merge(result.get(e2.getKey()), v2); + } + result.put(e2.getKey(), v2); + } + } + return result; + } + } + + /** interface for a construction instruction which may be invoked by one it wraps */ + public interface WrappingConstructionInstruction extends ConstructionInstruction { + /** constructors list has next-outermost first */ + public Maybe create(Class typeConstraintSoFar, @Nullable List innerConstructors); + } + + /** see {@link Factory#newUsingConstructorWithArgsAndArgsListMergeStrategy(Class, List, ConstructionInstruction, ObjectMergeStrategy)} + * and {@link Factory#newUsingConstructorWithArgsAndArgumentMergeStrategy(Class, List, ConstructionInstruction, ObjectMergeStrategy)} */ + public static abstract class AbstractConstructionWithArgsInstruction implements WrappingConstructionInstruction { + private final Class type; + private final List args; + private final WrappingConstructionInstruction outerInstruction; + + protected AbstractConstructionWithArgsInstruction(Class type, List args, @Nullable WrappingConstructionInstruction outerInstruction) { + this.type = type; + this.args = args; + this.outerInstruction = outerInstruction; + } + + @Override public Class getType() { return type; } + @Override public List getArgs() { return args; } + @Override public ConstructionInstruction getOuterInstruction() { return outerInstruction; } + + @Override + public Maybe create() { + return create(null, null); + } + + @Override + public Maybe create(Class typeConstraintSoFar, List constructorsSoFarOutermostFirst) { + if (typeConstraintSoFar==null) typeConstraintSoFar = type; + if (type!=null) { + if (typeConstraintSoFar==null || typeConstraintSoFar.isAssignableFrom(type)) typeConstraintSoFar = type; + else if (type.isAssignableFrom(typeConstraintSoFar)) { /* fine */ } + else { + throw new IllegalStateException("Incompatible expected types "+typeConstraintSoFar+" and "+type); + } + } + + if (outerInstruction!=null) { + if (constructorsSoFarOutermostFirst==null) constructorsSoFarOutermostFirst = MutableList.of(); + else constructorsSoFarOutermostFirst = MutableList.copyOf(constructorsSoFarOutermostFirst); + + constructorsSoFarOutermostFirst.add(0, this); + return outerInstruction.create(typeConstraintSoFar, constructorsSoFarOutermostFirst); + } + + return createWhenNoOuter(typeConstraintSoFar, constructorsSoFarOutermostFirst); + } + + protected Maybe createWhenNoOuter(Class typeConstraintSoFar, List constructorsSoFar) { + List combinedArgs = combineArguments(constructorsSoFar); + + if (typeConstraintSoFar==null) throw new IllegalStateException("No type information available"); + if ((typeConstraintSoFar.getModifiers() & (Modifier.ABSTRACT | Modifier.INTERFACE)) != 0) + throw new IllegalStateException("Insufficient type information: expected "+type+" is not directly instantiable"); + return Reflections.invokeConstructorFromArgsIncludingPrivate(typeConstraintSoFar, combinedArgs==null ? new Object[0] : combinedArgs.toArray()); + } + + protected abstract List combineArguments(@Nullable List constructorsSoFar); + } + + /** see {@link Factory#newDefault(Class, ConstructionInstruction)} and other methods in that factory class */ + protected static class BasicConstructionWithArgsInstruction extends AbstractConstructionWithArgsInstruction { + private ObjectMergeStrategy argsListMergeStrategy; + + protected BasicConstructionWithArgsInstruction(Class type, List args, @Nullable WrappingConstructionInstruction outerInstruction, + ObjectMergeStrategy argsListMergeStrategy) { + super(type, args, outerInstruction); + this.argsListMergeStrategy = argsListMergeStrategy; + } + + protected List combineArguments(@Nullable List constructorsSoFarOutermostFirst) { + List combinedArgs = null; + List constructorsInnermostFirst = MutableList.copyOf(constructorsSoFarOutermostFirst); + Collections.reverse(constructorsInnermostFirst); + + for (ConstructionInstruction i: constructorsInnermostFirst) { + combinedArgs = (List) argsListMergeStrategy.merge(combinedArgs, i.getArgs()); + } + combinedArgs = (List) argsListMergeStrategy.merge(combinedArgs, getArgs()); + return combinedArgs; + } + } + + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/SerializersOnBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/SerializersOnBlackboard.java new file mode 100644 index 0000000000..6183bff940 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/SerializersOnBlackboard.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; + +/** Stores serializers that should be used */ +public class SerializersOnBlackboard { + + private static final Logger log = LoggerFactory.getLogger(SerializersOnBlackboard.class); + + private static String KEY = SerializersOnBlackboard.class.getName(); + + public static boolean isPresent(Map blackboard) { + return blackboard.containsKey(KEY); + } + public static SerializersOnBlackboard get(Map blackboard) { + return Preconditions.checkNotNull(peek(blackboard), "Not yet available"); + } + public static SerializersOnBlackboard peek(Map blackboard) { + return (SerializersOnBlackboard) blackboard.get(KEY); + } + public static SerializersOnBlackboard create(Map blackboard) { + if (isPresent(blackboard)) { throw new IllegalStateException("Already present"); } + blackboard.put(KEY, new SerializersOnBlackboard()); + return peek(blackboard); + } + public static SerializersOnBlackboard getOrCreate(Map blackboard) { + SerializersOnBlackboard result = peek(blackboard); + if (result!=null) return result; + result = new SerializersOnBlackboard(); + blackboard.put(KEY, result); + return result; + } + + private List preSerializers = MutableList.of(); + private List instantiatedTypeSerializers = MutableList.of(); + private List expectedTypeSerializers = MutableList.of(); + private List postSerializers = MutableList.of(); + + public void addInstantiatedTypeSerializers(Iterable newInstantiatedTypeSerializers) { + addNewSerializers(instantiatedTypeSerializers, newInstantiatedTypeSerializers, "instantiated type"); + } + public void addExpectedTypeSerializers(Iterable newExpectedTypeSerializers) { + addNewSerializers(expectedTypeSerializers, newExpectedTypeSerializers, "expected type"); + + } + public void addPostSerializers(List newPostSerializers) { + addNewSerializers(postSerializers, newPostSerializers, "post"); + } + protected static void addNewSerializers(List addTo, Iterable elementsToAddIfNotPresent, String description) { + MutableSet newOnes = MutableSet.copyOf(elementsToAddIfNotPresent); + int sizeBefore = newOnes.size(); + // removal isn't expected to work as hashCode and equals aren't typically implemented; + // callers should make sure only to add when needed + newOnes.removeAll(addTo); + if (log.isTraceEnabled()) + log.trace("Adding "+newOnes.size()+" serializers ("+sizeBefore+" initially requested) for "+description+" (had "+addTo.size()+"): "+newOnes); + addTo.addAll(newOnes); + } + + public Iterable getSerializers() { + return Iterables.concat(preSerializers, instantiatedTypeSerializers, expectedTypeSerializers, postSerializers); + } + + public static boolean isAddedByTypeInstantiation(Map blackboard, YomlSerializer serializer) { + SerializersOnBlackboard sb = get(blackboard); + if (sb!=null && sb.instantiatedTypeSerializers.contains(serializer)) return true; + return false; + } + + public String toString() { + return super.toString()+"["+preSerializers.size()+" pre,"+instantiatedTypeSerializers.size()+" inst,"+ + expectedTypeSerializers.size()+" exp,"+postSerializers.size()+" post]"; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConfigs.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConfigs.java new file mode 100644 index 0000000000..7b0bda6420 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConfigs.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.util.Collection; +import java.util.List; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; +import org.apache.brooklyn.util.javalang.coerce.TypeCoercerExtensible; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.YomlTypeRegistry; +import org.apache.brooklyn.util.yoml.serializers.FieldsInMapUnderFields; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeEnum; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistry; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeList; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeMap; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypePrimitive; + +import com.google.common.collect.ImmutableList; + +public class YomlConfigs { + + private static class BasicYomlConfig implements YomlConfig { + private BasicYomlConfig() {} + private BasicYomlConfig(YomlConfig original) { + if (original!=null) { + this.typeRegistry = original.getTypeRegistry(); + this.coercer = original.getCoercer(); + this.serializersPost = original.getSerializersPost(); + } + } + + private YomlTypeRegistry typeRegistry; + private TypeCoercer coercer = TypeCoercerExtensible.newDefault(); + private List serializersPost = MutableList.of(); + + public YomlTypeRegistry getTypeRegistry() { + return typeRegistry; + } + + public TypeCoercer getCoercer() { + return coercer; + } + + public List getSerializersPost() { + return ImmutableList.copyOf(serializersPost); + } + } + + public static class Builder> { + + final BasicYomlConfig result; + protected Builder() { result = new BasicYomlConfig(); } + protected Builder(YomlConfig source) { result = new BasicYomlConfig(source); } + + @SuppressWarnings("unchecked") + T thiz = (T) this; + public T typeRegistry(YomlTypeRegistry tr) { result.typeRegistry = tr; return thiz; } + public T coercer(TypeCoercer x) { result.coercer = x; return thiz; } + public T serializersPostReplace(List x) { result.serializersPost = x; return thiz; } + public T serializersPostAdd(Collection x) { result.serializersPost.addAll(x); return thiz; } + public T serializersPostAddDefaults() { return serializersPostAdd(getDefaultSerializers()); } + + public YomlConfig build() { return new BasicYomlConfig(result); } + + public static List getDefaultSerializers() { + return MutableList.of( + new FieldsInMapUnderFields(), + new InstantiateTypePrimitive(), + new InstantiateTypeEnum(), + new InstantiateTypeList(), + new InstantiateTypeMap(), + new InstantiateTypeFromRegistry() ); + } + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContext.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContext.java new file mode 100644 index 0000000000..e4e4968332 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContext.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; + +import com.google.common.base.Objects; + +public abstract class YomlContext { + + final YomlContext parent; + final String jsonPath; + final String expectedType; + Object javaObject; + Object yamlObject; + Map blackboard; + + String phaseCurrent = null; + int phaseCurrentStep = -1; + Set phasesFollowing = MutableSet.of(StandardPhases.MANIPULATING, StandardPhases.HANDLING_TYPE, StandardPhases.HANDLING_TYPE, StandardPhases.HANDLING_FIELDS); + List phasesPreceding = MutableList.of(); + + ConstructionInstruction constructionInstruction; + + public static interface StandardPhases { + String MANIPULATING = "manipulating"; + String HANDLING_TYPE = "handling-type"; + String HANDLING_FIELDS = "handling-fields"; + } + + public YomlContext(String jsonPath, String expectedType, YomlContext parent) { + this.jsonPath = jsonPath; + this.expectedType = expectedType; + this.parent = parent; + } + + public YomlContext getParent() { + return parent; + } + /** empty string if the root, otherwise a path string using e.g. /foo[0][0]/bar notation + * for evaluation of baz in { foo: [ [ { bar: baz } ] ] } */ + public String getJsonPath() { + return jsonPath; + } + public String getExpectedType() { + return expectedType; + } + public Object getJavaObject() { + return javaObject; + } + public void setJavaObject(Object javaObject) { + this.javaObject = javaObject; + } + + public Object getYamlObject() { + return yamlObject; + } + /** Sets the YAML object that will be returned from a write. + * In special cases of major YAML transformation this may also be used during read + * but most minor modifications should use blackboard objects. */ + public void setYamlObject(Object yamlObject) { + this.yamlObject = yamlObject; + } + + public boolean isPhase(String phase) { return Objects.equal(phase, phaseCurrent); } + public boolean seenPhase(String phase) { return phasesPreceding.contains(phase); } + public boolean willDoPhase(String phase) { return phasesFollowing.contains(phase); } + public String phaseCurrent() { return phaseCurrent; } + public int phaseCurrentStep() { return phaseCurrentStep; } + public int phaseStepAdvance() { + if (phaseCurrentStep() < Integer.MAX_VALUE) phaseCurrentStep++; + return phaseCurrentStep(); + } + public boolean phaseAdvance() { + if (phaseCurrent!=null) phasesPreceding.add(phaseCurrent); + Iterator fi = phasesFollowing.iterator(); + if (!fi.hasNext()) { + phaseCurrent = null; + phaseCurrentStep = Integer.MAX_VALUE; + return false; + } + phaseCurrent = fi.next(); + phasesFollowing = MutableSet.copyOf(fi); + phaseCurrentStep = -1; + return true; + } + public void phaseRestart() { phaseCurrentStep = -1; } + public void phaseInsert(String nextPhase, String ...otherNextPhases) { + phasesFollowing = MutableSet.of(nextPhase).putAll(Arrays.asList(otherNextPhases)).putAll(phasesFollowing); + } + public void phasesFinished() { + if (phaseCurrent!=null) phasesPreceding.add(phaseCurrent); + phasesFollowing = MutableSet.of(); phaseAdvance(); + } + + public ConstructionInstruction getConstructionInstruction() { + return constructionInstruction; + } + + @Override + public String toString() { + return super.toString()+"["+getJsonPath()+"]"; + } + + public Map getBlackboard() { + if (blackboard==null) blackboard = MutableMap.of(); + return blackboard; + } + + public abstract YomlContext subpath(String subpath, Object newItem, String optionalType); + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForRead.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForRead.java new file mode 100644 index 0000000000..e958fea02d --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForRead.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import org.apache.brooklyn.util.text.Strings; + +public class YomlContextForRead extends YomlContext { + + public YomlContextForRead(Object yamlObject, String jsonPath, String expectedType, YomlContext parent) { + super(jsonPath, expectedType, parent); + setYamlObject(yamlObject); + } + + @Override + public YomlContextForRead subpath(String subpath, Object newItem, String superType) { + return new YomlContextForRead(newItem, getJsonPath()+subpath, superType, this); + } + + String origin; + int offset; + int length; + + @Override + public String toString() { + return "reading"+(expectedType!=null ? " "+expectedType : "")+" at "+(Strings.isNonBlank(jsonPath) ? jsonPath : "root"); + } + + public YomlContextForRead constructionInstruction(ConstructionInstruction newConstruction) { + YomlContextForRead result = new YomlContextForRead(yamlObject, jsonPath, expectedType, parent); + result.constructionInstruction = newConstruction; + return result; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForWrite.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForWrite.java new file mode 100644 index 0000000000..8762506f4f --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlContextForWrite.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +public class YomlContextForWrite extends YomlContext { + + public YomlContextForWrite(Object javaObject, String jsonPath, String expectedType, YomlContext parent) { + super(jsonPath, expectedType, parent); + setJavaObject(javaObject); + } + + @Override + public YomlContextForWrite subpath(String subpath, Object newItem, String optionalType) { + return new YomlContextForWrite(newItem, getJsonPath()+subpath, optionalType, this); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConverter.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConverter.java new file mode 100644 index 0000000000..93ad113406 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlConverter.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.util.Map; +import java.util.Objects; + +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlRequirement; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistry; +import org.apache.brooklyn.util.yoml.serializers.ReadingTypeOnBlackboard; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Iterables; + +public class YomlConverter { + + private static final Logger log = LoggerFactory.getLogger(YomlConverter.class); + private final YomlConfig config; + + /** easy way at dev time to get trace logging to stdout info level */ + private static boolean FORCE_SHOW_TRACE_LOGGING = false; + + public YomlConverter(YomlConfig config) { + this.config = config; + } + + /** + * returns object of type expectedType + * makes shallow copy of the object, then goes through serializers modifying it or creating/setting result, + * until result is done + */ + public Object read(YomlContextForRead context) { + loopOverSerializers(context); + return context.getJavaObject(); + } + + /** + * returns jsonable object (map, list, primitive) + */ + public Object write(final YomlContextForWrite context) { + loopOverSerializers(context); + return context.getYamlObject(); + } + + protected boolean isTraceDetailWanted() { + return log.isTraceEnabled() || FORCE_SHOW_TRACE_LOGGING; + } + protected void logTrace(String message) { + if (FORCE_SHOW_TRACE_LOGGING) { + log.info(message); + } else { + log.trace(message); + } + } + + protected void loopOverSerializers(YomlContext context) { + // TODO refactor further so we always pass the context + Map blackboard = context.getBlackboard(); + + // find the serializers known so far; store on blackboard so they could be edited + SerializersOnBlackboard serializers = SerializersOnBlackboard.getOrCreate(blackboard); + if (context.getExpectedType()!=null) { + serializers.addExpectedTypeSerializers(config.getTypeRegistry().getSerializersForType(context.getExpectedType(), context)); + } + serializers.addPostSerializers(config.getSerializersPost()); + + if (context instanceof YomlContextForRead) { + // read needs instantiated so that these errors display before manipulating errors and others + ReadingTypeOnBlackboard.get(blackboard); + } + + String lastYamlObject = ""+context.getYamlObject(); + String lastJavaObject = ""+context.getJavaObject(); + Map lastBlackboardOutput = MutableMap.of(); + if (isTraceDetailWanted()) { + logTrace("YOML "+contextMode(context)+" "+contextSummary(context)+" (expecting "+context.getExpectedType()+")"); + showBlackboard(blackboard, lastBlackboardOutput, false); + } + + while (context.phaseAdvance()) { + while (context.phaseStepAdvance() "+context.getYamlObject()); + checkCompletion(context); + } + + protected String contextMode(YomlContext context) { + return context instanceof YomlContextForWrite ? "write" : "read"; + } + + private void showBlackboard(Map blackboard, Map lastBlackboardOutput, boolean justDelta) { + Map newBlackboardOutput = MutableMap.copyOf(lastBlackboardOutput); + if (!justDelta) lastBlackboardOutput.clear(); + + for (Map.Entry bb: blackboard.entrySet()) { + String k = cleanName(bb.getKey()); + String v = cleanName(bb.getValue()); + newBlackboardOutput.put(k, v); + String last = lastBlackboardOutput.remove(k); + if (!justDelta) logTrace(" "+k+": "+v); + else if (!Objects.equals(last, v)) logTrace(" "+k+" "+(last==null ? "added" : "now")+": "+v); + } + + for (String k: lastBlackboardOutput.keySet()) { + logTrace(" "+k+" removed"); + } + + lastBlackboardOutput.putAll(newBlackboardOutput); + } + + protected String cleanName(Object s) { + String out = Strings.toString(s); + out = Strings.removeFromStart(out, YomlConverter.class.getPackage().getName()); + out = Strings.removeFromStart(out, InstantiateTypeFromRegistry.class.getPackage().getName()); + out = Strings.removeFromStart(out, "."); + return out; + } + + protected String contextSummary(YomlContext context) { + return (Strings.isBlank(context.getJsonPath()) ? "/" : context.getJsonPath()) + " = " + + (context instanceof YomlContextForWrite ? context.getJavaObject() : context.getYamlObject()); + } + + protected void checkCompletion(YomlContext context) { + for (Object bo: context.getBlackboard().values()) { + if (bo instanceof YomlRequirement) { + ((YomlRequirement)bo).checkCompletion(context); + } + } + } + + + /** + * generates human-readable schema for a type + */ + public String document(String type) { + // TODO + return null; + } + + public YomlConfig getConfig() { + return config; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlUtils.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlUtils.java new file mode 100644 index 0000000000..43860c42a5 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/internal/YomlUtils.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.javalang.FieldOrderings; +import org.apache.brooklyn.util.javalang.ReflectionPredicates; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yaml.Yamls; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlTypeRegistry; +import org.apache.brooklyn.util.yoml.annotations.DefaultKeyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import com.google.common.reflect.TypeToken; + +public class YomlUtils { + + private static final Logger log = LoggerFactory.getLogger(YomlUtils.class); + + /** true iff k1 and k2 are case-insensitively equal after removing all - and _. + * Note that the definition of mangling may change. + * TODO it should be stricter so that "ab" and "a-b" don't match but "aB" and "a-b" and "a_b" do */ + @Beta + public static boolean mangleable(String k1, String k2) { + if (k1==null || k2==null) return k1==k2; + k1 = Strings.replaceAllNonRegex(k1, "-", ""); + k1 = Strings.replaceAllNonRegex(k1, "_", ""); + k2 = Strings.replaceAllNonRegex(k2, "-", ""); + k2 = Strings.replaceAllNonRegex(k2, "_", ""); + return k1.toLowerCase().equals(k2.toLowerCase()); + } + + /** type marker that value can be kept in its as-read form */ + public final static String TYPE_JSON = "json"; + + public final static String TYPE_STRING = "string"; + public final static String TYPE_OBJECT = "object"; + public final static String TYPE_MAP = "map"; + public final static String TYPE_LIST = "list"; + public final static String TYPE_SET = "set"; + + public final static class JsonMarker { + public static final String TYPE = TYPE_JSON; + + /** true IFF o is a json primitive or map/iterable consisting of pure json items, + * with the additional constraint that map keys must be strings */ + public static boolean isPureJson(Object o) { + if (o==null || Boxing.isPrimitiveOrBoxedObject(o)) return true; + if (o instanceof String) return true; + if (o instanceof Iterable) { + for (Object oi: ((Iterable)o)) { + if (!isPureJson(oi)) return false; + } + return true; + } + if (o instanceof Map) { + for (Map.Entry oi: ((Map)o).entrySet()) { + if (!(oi.getKey() instanceof String)) return false; + if (!isPureJson(oi.getValue())) return false; + } + return true; + } + return false; + } + } + + /** parses a type string and if it is generic it gives access to the underlying types */ + public static class GenericsParse { + public String warning; + public boolean isGeneric = false; + public String baseType; + public List subTypes = MutableList.of(); + + public GenericsParse(String type) { + if (type==null) return; + + baseType = type.trim(); + int genericStart = baseType.indexOf('<'); + if (genericStart > 0) { + isGeneric = true; + + if (!parse(baseType.substring(genericStart))) { + warning = "Invalid generic type "+baseType; + return; + } + + baseType = baseType.substring(0, genericStart); + } + } + + private boolean parse(String s) { + int depth = 0; + boolean inWord = false; + int lastWordStart = -1; + for (int i=0; i') { + if (c==',' && depth==0) return false; + if (c=='>') { depth--; } + if (depth>1) continue; + // depth 1 word end, either due to , or due to > + if (c==',' && !inWord) return false; + subTypes.add(s.substring(lastWordStart, i).trim()); + inWord = false; + continue; + } + if (!inWord) { + if (depth!=1) return false; + inWord = true; + lastWordStart = i; + } + } + // finished. expect depth 0 and not in word + return depth==0 && !inWord; + } + + public boolean isGeneric() { return isGeneric; } + public int subTypeCount() { return subTypes.size(); } + } + + public static String getTypeNameWithGenerics(TypeToken t, YomlTypeRegistry tr) { + return getTypeNameWithGenerics(t.getType(), tr); + } + + @SuppressWarnings("serial") private static class CannotResolveGenerics extends IllegalStateException {} + private final static Set WARNED_ON_UNSUPPORTED_GENERICS = MutableSet.of(); + + public static String getTypeNameWithGenerics(Type t, YomlTypeRegistry tr) { + if (t==null) return null; + + if (t instanceof ParameterizedType) { + String result = getTypeNameWithGenerics( ((ParameterizedType)t).getRawType(), tr ); + try { + StringBuilder sb = new StringBuilder(result); + Type[] args = ((ParameterizedType)t).getActualTypeArguments(); + if (args==null || args.length==0) { + // nothing + } else { + sb.append("<"); + sb.append(getTypeNameWithGenerics( args[0], tr )); + for (int i=1; i"); + } + return sb.toString(); + } catch (CannotResolveGenerics e) { + // fall back to non-generic + return result; + } + } + + if (t instanceof Class) { + return tr.getTypeNameOfClass((Class)t); + } + + // don't support WilcardType, BoundedType, or arrays + String tn = t.getClass().getName(); + if (WARNED_ON_UNSUPPORTED_GENERICS.contains(tn)) { + log.warn("Unsupported generic type: "+t+" ("+tn+"), falling back to raw type (only logging once)"); + } + throw new CannotResolveGenerics(); + } + + public static Map getAllNonTransientNonStaticFields(Class type, T optionalInstanceToRequireNonNullFieldValue) { + return getAllFields(type, optionalInstanceToRequireNonNullFieldValue, + Predicates.and(ReflectionPredicates.IS_FIELD_NON_TRANSIENT, ReflectionPredicates.IS_FIELD_NON_STATIC)); + } + public static Map getAllNonTransientStaticFields(Class type) { + return getAllFields(type, null, + Predicates.and(ReflectionPredicates.IS_FIELD_NON_TRANSIENT, ReflectionPredicates.IS_FIELD_STATIC)); + } + + /** Finds all fields on a type, including inherited, including statics from interfaces, subject to the optional filter, + * and optionally requiring a non-null value for the field on a given instant. + * These are ordered in {@link FieldOrderings#ALPHABETICAL_FIELD_THEN_SUB_BEST_FIRST} order, + * with shadowed fields prefixed by the name of the superclass and ".". + * + * @param type Class to scan + * @param optionalInstanceToRequireNonNullFieldValue An instance, which if supplied, is used to exclude + * fields for which this instance has a null value + * @param filter Filter to apply on fields + * @return + */ + public static Map getAllFields(Class type, @Nullable T optionalInstanceToRequireNonNullFieldValue, @Nullable Predicate filter) { + Map result = MutableMap.of(); + List fields = Reflections.findFields(type, + null, + FieldOrderings.ALPHABETICAL_FIELD_THEN_SUB_BEST_FIRST); + Field lastF = null; + for (Field f: fields) { + if (filter==null || filter.apply(f)) { + if (optionalInstanceToRequireNonNullFieldValue==null || + Reflections.getFieldValueMaybe(optionalInstanceToRequireNonNullFieldValue, f).isPresentAndNonNull()) { + String name = f.getName(); + if (lastF!=null && lastF.getName().equals(f.getName())) { + // if field is shadowed use FQN + String fqn = f.getDeclaringClass().getCanonicalName(); + if (Strings.isBlank(fqn)) fqn = f.getDeclaringClass().getName(); + name = fqn+"."+name; + } + result.put(name, f); + } + } + lastF = f; + } + return result; + } + public static List getAllNonTransientNonStaticFieldNames(Class type, T optionalInstanceToRequireNonNullFieldValue) { + return MutableList.copyOf(getAllNonTransientNonStaticFields(type, optionalInstanceToRequireNonNullFieldValue).keySet()); + } + + @SuppressWarnings("unchecked") + public static List getAllNonTransientNonStaticFieldNamesUntyped(Class type, Object optionalInstanceToRequireNonNullFieldValue) { + return getAllNonTransientNonStaticFieldNames((Class)type, optionalInstanceToRequireNonNullFieldValue); + } + + /** + * Provides poor man's generics -- we decorate when looking at a field, + * and strip when looking up in the registry. + *

+ * It's not that bad as fields are the *only* place in java where generic information is available. + *

+ * However we don't do them recursively at all (so eg a List> becomes a List). + * TODO That wouldn't be hard to fix. + */ + public static String getFieldTypeName(Field ff, YomlConfig config) { + String baseTypeName = config.getTypeRegistry().getTypeNameOfClass(ff.getType()); + String typeName = baseTypeName; + Type type = ff.getGenericType(); + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType)type; + if (pt.getActualTypeArguments().length>0) { + typeName += "<"; + for (int i=0; i0) typeName += ","; + Type ft = pt.getActualTypeArguments()[i]; + Class fc = null; + if (fc==null && ft instanceof ParameterizedType) ft = ((ParameterizedType)ft).getRawType(); + if (fc==null && ft instanceof Class) fc = (Class)ft; + String rfc = config.getTypeRegistry().getTypeNameOfClass(fc); + if (rfc==null) { + // cannot resolve generics + return baseTypeName; + } + typeName += rfc; + } + typeName += ">"; + } + } + return typeName; + } + + /** add the given defaults to the target, ignoring any where the key is already present; returns number added */ + public static int addDefaults(Map defaults, Map target) { + int i=0; + if (defaults!=null) for (String key: defaults.keySet()) { + if (!target.containsKey(key)) { + target.put(key, defaults.get(key)); + i++; + } + } + return i; + } + + + /** removes the given defaults from the target, where the key and value match, + * ignoring any where the key is already present; returns number removed */ + public static int removeDefaults(Map defaults, Map target) { + int i=0; + if (defaults!=null && target!=null) for (String key: defaults.keySet()) { + if (target.containsKey(key)) { + Object v = target.get(key); + Object dv = defaults.get(key); + if (Objects.equal(v, dv)) { + target.remove(key); + i++; + } + } + } + return i; + } + + public static Map extractDefaultMap(DefaultKeyValue[] defaultValues) { + if (defaultValues==null || defaultValues.length==0) return null; + MutableMap result = MutableMap.of(); + for (DefaultKeyValue d: defaultValues) { + Object v = d.val(); + if (d.valNeedsParsing()) { + v = Iterables.getOnlyElement( Yamls.parseAll(d.val()) ); + } + result.put(d.key(), v); + } + return result; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/AllFieldsTopLevel.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/AllFieldsTopLevel.java new file mode 100644 index 0000000000..a9ebd52ea7 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/AllFieldsTopLevel.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAnnotations; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; + +/** Adds {@link TopLevelFieldSerializer} instances for all fields declared on the type */ +@Alias("all-fields-top-level") +public class AllFieldsTopLevel extends YomlSerializerComposition { + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + /** marker on blackboard indicating that we have run */ + static class DoneAllFieldsTopLevel {} + + public class Worker extends YomlSerializerWorker { + + public void read() { run(); } + public void write() { run(); } + + protected void run() { + if (!hasJavaObject()) return; + if (blackboard.containsKey(DoneAllFieldsTopLevel.class.getName())) return; + + // mark done + blackboard.put(DoneAllFieldsTopLevel.class.getName(), new DoneAllFieldsTopLevel()); + + SerializersOnBlackboard.get(blackboard).addInstantiatedTypeSerializers( + new YomlAnnotations().findTopLevelFieldSerializers(getJavaObject().getClass(), false)); + + context.phaseRestart(); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConfigInMapUnderConfigSerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConfigInMapUnderConfigSerializer.java new file mode 100644 index 0000000000..e10a3f9d7d --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConfigInMapUnderConfigSerializer.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +import com.google.common.reflect.TypeToken; + +public class ConfigInMapUnderConfigSerializer extends FieldsInMapUnderFields { + + final String keyNameForConfigWhenSerialized; + + public ConfigInMapUnderConfigSerializer(String keyNameForConfigWhenSerialized) { + this.keyNameForConfigWhenSerialized = keyNameForConfigWhenSerialized; + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + @Override + protected String getExpectedPhaseRead() { + return YomlContext.StandardPhases.MANIPULATING; + } + + @Override + protected String getKeyNameForMapOfGeneralValues() { + return keyNameForConfigWhenSerialized; + } + + public class Worker extends FieldsInMapUnderFields.Worker { + + @Override + public void read() { + if (!context.willDoPhase( + InstantiateTypeFromRegistryUsingConfigMap.PHASE_INSTANTIATE_TYPE_DEFERRED)) return; + if (JavaFieldsOnBlackboard.peek(blackboard, getKeyNameForMapOfGeneralValues())==null) return; + + super.read(); + } + + protected boolean shouldHaveJavaObject() { return false; } + + @Override + protected boolean setKeyValueForJavaObjectOnRead(String key, Object value, String optionalTypeConstraint) throws IllegalAccessException { + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard, getKeyNameForMapOfGeneralValues()); + String optionalType = getType(key, null); + ConfigKey cKey = getKey(key); + + optionalType = merge(key, cKey==null ? null : cKey.getType(), optionalType, optionalTypeConstraint); + + Object v2; + try { + if (isDeferredValue(value)) v2 = value; + else v2 = converter.read( ((YomlContextForRead)context).subpath("/"+key, value, optionalType) ); + } catch (Exception e) { + // for config we try with the optional type, but don't insist + Exceptions.propagateIfFatal(e); + if (optionalType!=null) optionalType = null; + try { + v2 = converter.read( ((YomlContextForRead)context).subpath("/"+key, value, optionalType) ); + } catch (Exception e2) { + Exceptions.propagateIfFatal(e2); + throw e; + } + } + fib.fieldsFromReadToConstructJava.put(key, v2); + return true; + } + + protected Map writePrepareGeneralMap() { + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard); + if (fib==null || fib.configToWriteFromJava==null) return null; + Map configMap = MutableMap.of(); + + for (Map.Entry entry: fib.configToWriteFromJava.entrySet()) { + String optionalType = getType(entry.getKey(), entry.getValue()); + ConfigKey cKey = getKey(entry.getKey()); + + // can we record additional information about the type in the yaml? + // TODO merge with similar code in overwritten method + String tf = TypeFromOtherFieldBlackboard.get(blackboard).getTypeConstraintField(entry.getKey()); + if (tf!=null) { + if (cKey!=null && !Object.class.equals(cKey.getType())) { + // currently we only support smart types if the base type is object; + // see getFieldTypeName + + } else { + if (!TypeFromOtherFieldBlackboard.get(blackboard).isTypeConstraintFieldReal(entry.getKey())) { + String realType = config.getTypeRegistry().getTypeName(entry.getValue()); + optionalType = realType; + // for non-real, just write the pseudo-type-field at root + getOutputYamlMap().put(tf, realType); + + } else { + Object rt = fib.configToWriteFromJava.get(tf); + if (rt!=null) { + if (rt instanceof String) { + optionalType = (String) rt; + } else { + throw new YomlException("Cannot use type information from "+tf+" for "+cKey+" as it is "+rt, context); + } + } + } + } + } + + Object v = converter.write( ((YomlContextForWrite)context).subpath("/"+entry.getKey(), entry.getValue(), optionalType) ); + configMap.put(entry.getKey(), v); + } + for (String key: configMap.keySet()) fib.configToWriteFromJava.remove(key); + + return configMap; + } + + @Nullable protected String getType(String keyName, Object value) { + ConfigKey keyForTypeInfo = getKey(keyName); + TypeToken type = keyForTypeInfo==null ? null : keyForTypeInfo.getTypeToken(); + String optionalType = null; + if (type!=null && (value==null || type.getRawType().isInstance(value))) + optionalType = YomlUtils.getTypeNameWithGenerics(type, config.getTypeRegistry()); + return optionalType; + } + + @Nullable protected ConfigKey getKey(String keyName) { + TopLevelFieldsBlackboard efb = TopLevelFieldsBlackboard.get(blackboard, getKeyNameForMapOfGeneralValues()); + ConfigKey typeKey = efb.getConfigKey(keyName); + return typeKey; + } + + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertFromPrimitive.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertFromPrimitive.java new file mode 100644 index 0000000000..d70cdd7fc5 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertFromPrimitive.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlFromPrimitive; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +@YomlAllFieldsTopLevel +@Alias("convert-from-primitive") +public class ConvertFromPrimitive extends YomlSerializerComposition { + + public final static String DEFAULT_DEFAULT_KEY = ConvertSingletonMap.DEFAULT_KEY_FOR_VALUE; + + public ConvertFromPrimitive() { } + + public ConvertFromPrimitive(YomlFromPrimitive ann) { + this(ann.keyToInsert(), YomlUtils.extractDefaultMap(ann.defaults())); + } + + public ConvertFromPrimitive(String keyToInsert, Map defaults) { + super(); + this.keyToInsert = keyToInsert; + this.defaults = defaults; + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + @Alias("key") + String keyToInsert = DEFAULT_DEFAULT_KEY; + Map defaults; + + public class Worker extends YomlSerializerWorker { + public void read() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + // runs before type instantiated + if (hasJavaObject()) return; + // only runs on primitives/lists + if (!isJsonPrimitiveObject(getYamlObject()) && !isJsonList(getYamlObject())) return; + + Map newYamlMap = MutableMap.of(keyToInsert, getYamlObject()); + + YomlUtils.addDefaults(defaults, newYamlMap); + + context.setYamlObject(newYamlMap); + context.phaseRestart(); + } + + public void write() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + if (!isYamlMap()) return; + // don't run if we're only added after instantiating the type (because then we couldn't read back!) + if (SerializersOnBlackboard.isAddedByTypeInstantiation(blackboard, ConvertFromPrimitive.this)) return; + + Object value = getOutputYamlMap().get(keyToInsert); + if (value==null) return; + if (!isJsonPrimitiveObject(value) && !isJsonList(value)) return; + + Map yamlMap = MutableMap.copyOf(getOutputYamlMap()); + yamlMap.remove(keyToInsert); + YomlUtils.removeDefaults(defaults, yamlMap); + if (!yamlMap.isEmpty()) return; + + context.setYamlObject(value); + context.phaseRestart(); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertSingletonMap.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertSingletonMap.java new file mode 100644 index 0000000000..80dfb7d375 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ConvertSingletonMap.java @@ -0,0 +1,365 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlSingletonMap; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.GenericsParse; + +import com.google.common.collect.Iterables; + +/* + * key-for-key: type + * key-for-primitive-value: type || key-for-any-value: ... || key-for-list-value: || key-for-map-value + * || merge-with-map-value + * defaults: { type: top-level-field } + */ +@YomlAllFieldsTopLevel +@Alias("convert-singleton-map") +public class ConvertSingletonMap extends YomlSerializerComposition { + + public static enum SingletonMapMode { LIST_AS_MAP, LIST_AS_LIST, NON_LIST } + + public ConvertSingletonMap() { } + + public ConvertSingletonMap(YomlSingletonMap ann) { + this(ann.keyForKey(), ann.keyForAnyValue(), ann.keyForPrimitiveValue(), ann.keyForListValue(), ann.keyForMapValue(), + null, null, YomlUtils.extractDefaultMap(ann.defaults())); + } + + public ConvertSingletonMap(String keyForKey, String keyForAnyValue, String keyForPrimitiveValue, String keyForListValue, String keyForMapValue, + Collection modes, Boolean mergeWithMapValue, Map defaults) { + super(); + this.keyForKey = keyForKey; + this.keyForAnyValue = keyForAnyValue; + this.keyForPrimitiveValue = keyForPrimitiveValue; + this.keyForListValue = keyForListValue; + this.keyForMapValue = keyForMapValue; + this.onlyInModes = MutableSet.copyOf(modes); + this.mergeWithMapValue = mergeWithMapValue; + this.defaults = defaults; + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public final static String DEFAULT_KEY_FOR_KEY = ".key"; + public final static String DEFAULT_KEY_FOR_VALUE = ".value"; + + String keyForKey = DEFAULT_KEY_FOR_KEY; + /** key to use if type-specific key is not known; + * only applies to map if {@link #mergeWithMapValue} is set */ + String keyForAnyValue = DEFAULT_KEY_FOR_VALUE; + String keyForPrimitiveValue; + String keyForListValue; + /** if the value against the single key is itself a map, treat it as a value for a key wit this name */ + String keyForMapValue; + /** conveniences for {@link #onlyInModes} when just supplying one */ + SingletonMapMode onlyInMode = null; + /** if non-empty, restrict the modes where this serializer can run */ + Set onlyInModes = null; + /** if the value against the single key is itself a map, should we put the {@link #keyForKey} as another entry in the map + * to get the result; the default (if null) and usual behaviour is to do so but not if {@link #keyForMapValue} is set (because we'd use that), and not if there would be a collision + * (ie {@link #keyForKey} is already present) and we have a value for {@link #keyForAnyValue} (so we can use that); + * however we can set true/false to say always merge (which will ignore {@link #keyForMapValue}) or never merge + * (which will prevent this serializer from applying unless {@link #keyForMapValue} or {@link #keyForAnyValue} is set) */ + Boolean mergeWithMapValue; + Map defaults; + + public class Worker extends YomlSerializerWorker { + public void read() { + // runs before type instantiated + if (hasJavaObject()) return; + + if (context.isPhase(InstantiateTypeList.MANIPULATING_TO_LIST)) { + if (isYamlMap() && enterModeRead(SingletonMapMode.LIST_AS_MAP)) { + readManipulatingMapToList(); + } else if (getYamlObject() instanceof Collection && enterModeRead(SingletonMapMode.LIST_AS_LIST)) { + // this would also be done by instantiate-type-list, but it wouldn't know to + // pass this serializer through + readManipulatingInList(); + } + + return; + } + + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + + if (!isYamlMap()) return; + if (getRawInputYamlMap().size()!=1) return; + + if (!enterModeRead(SingletonMapMode.NON_LIST)) return; + + Object key = Iterables.getOnlyElement(getRawInputYamlMap().keySet()); + Object value = Iterables.getOnlyElement(getRawInputYamlMap().values()); + + // key should always be primitive + if (!isJsonPrimitiveObject(key)) return; + + Map newYamlMap = MutableMap.of(); + + newYamlMap.put(keyForKey, key); + + if (isJsonPrimitiveObject(value) && Strings.isNonBlank(keyForPrimitiveValue)) { + newYamlMap.put(keyForPrimitiveValue, value); + } else if (value instanceof Map) { + Boolean merge = isForMerging(value); + if (merge==null) return; + if (merge) { + newYamlMap.putAll((Map)value); + } else { + String keyForThisMap = Strings.isNonBlank(keyForMapValue) ? keyForMapValue : keyForAnyValue; + if (Strings.isBlank(keyForThisMap)) { + throw new IllegalStateException("Error in isForMergingLogic"); + } + newYamlMap.put(keyForThisMap, value); + } + } else if (value instanceof Iterable && Strings.isNonBlank(keyForListValue)) { + newYamlMap.put(keyForListValue, value); + } else { + newYamlMap.put(keyForAnyValue, value); + } + + YomlUtils.addDefaults(defaults, newYamlMap); + + context.setYamlObject(newYamlMap); + YamlKeysOnBlackboard.delete(blackboard); + context.phaseRestart(); + } + + protected boolean enterModeRead(SingletonMapMode newMode) { + SingletonMapMode currentMode = (SingletonMapMode) blackboard.get(ConvertSingletonMap.this); + if (currentMode==null) { + if (!allowInMode(newMode)) return false; + } else if (currentMode == SingletonMapMode.NON_LIST) { + // cannot transition from non-list mode others + return false; + } else { + // current mode is one of the list modes; + // only allowed to transition to non-list (in recursive call) + if (newMode == SingletonMapMode.NON_LIST) /* fine */; + else if (currentMode == SingletonMapMode.LIST_AS_MAP && newMode == SingletonMapMode.LIST_AS_LIST) ; + else { + return false; + } + } + // set the new mode + blackboard.put(ConvertSingletonMap.this, newMode); + return true; + } + + protected void readManipulatingInList() { + // go through a list, applying to each + List result = readManipulatingInList((Collection)getYamlObject(), SingletonMapMode.LIST_AS_LIST); + if (result==null) return; + + context.setYamlObject(result); + context.phaseAdvance(); + } + + protected List readManipulatingInList(Collection list, SingletonMapMode mode) { + // go through a list, applying to each + GenericsParse gp = new GenericsParse(context.getExpectedType()); + if (gp.warning!=null) { + warn(gp.warning); + return null; + } + String genericSubType = null; + if (gp.isGeneric()) { + if (gp.subTypeCount()!=1) { + // not a list + return null; + } + genericSubType = Iterables.getOnlyElement(gp.subTypes); + } + + List result = MutableList.of(); + int index = 0; + for (Object item: list) { + YomlContextForRead newContext = ((YomlContextForRead)context).subpath("["+index+"]", item, genericSubType); + // add this serializer and set mode in the new context + SerializersOnBlackboard.create(newContext.getBlackboard()).addExpectedTypeSerializers(MutableList.of((YomlSerializer) ConvertSingletonMap.this)); + newContext.getBlackboard().put(ConvertSingletonMap.this, mode); + + Object newItem = converter.read(newContext); + result.add( newItem ); + index++; + } + return result; + } + + protected void readManipulatingMapToList() { + // convert from a map to a list; then manipulate in list + List result = MutableList.of(); + for (Map.Entry entry: getRawInputYamlMap().entrySet()) { + result.add(MutableMap.of(entry.getKey(), entry.getValue())); + } + result = readManipulatingInList(result, SingletonMapMode.LIST_AS_MAP); + if (result==null) return; + + context.setYamlObject(result); + YamlKeysOnBlackboard.delete(blackboard); + context.phaseAdvance(); + } + + /** return true/false whether to merge, or null if need to bail out */ + protected Boolean isForMerging(Object value) { + if (mergeWithMapValue==null) { + // default merge logic (if null): + // * merge if there is no key-for-map-value AND + // * it's safe, ie there is no collision at the key-for-key key + // if not safe, we bail out (collisions may be used by clients to suppress) + if (Strings.isNonBlank(keyForMapValue)) return false; + if (((Map)value).containsKey(keyForKey)) return null; + return true; + } else { + return mergeWithMapValue; + } + } + + String OUR_PHASE = "manipulate-convert-singleton"; + + public void write() { + if (!isYamlMap()) return; + // don't run if we're only added after instantiating the type (because then we couldn't read back!) + if (SerializersOnBlackboard.isAddedByTypeInstantiation(blackboard, ConvertSingletonMap.this)) return; + + if (context.isPhase(YomlContext.StandardPhases.MANIPULATING) && !context.seenPhase(OUR_PHASE)) { + // finish manipulations before seeking to apply this + context.phaseInsert(OUR_PHASE); + return; + } + if (!context.isPhase(OUR_PHASE)) return; + + // don't run multiple times + if (blackboard.put(ConvertSingletonMap.this, SingletonMapMode.NON_LIST)!=null) return; + + if (!allowInMode(SingletonMapMode.NON_LIST)) { + if (!allowInMode(SingletonMapMode.LIST_AS_LIST)) { + // we can only write in one of the above two modes currently + // (list-as-map is difficult to reverse-engineer, and not necessary) + return; + } + YomlContext parent = context.getParent(); + if (parent==null || !(parent.getJavaObject() instanceof Collection)) { + // parent is not a list; disallow + return; + } + } + + if (!getOutputYamlMap().containsKey(keyForKey)) return; + Object newKey = getOutputYamlMap().get(keyForKey); + if (!isJsonPrimitiveObject(newKey)) { + // NB this is potentially irreversible - + // e.g. if given say we want for { color: red, xxx: yyy } to write { red: { xxx: yyy } } + // but if we have { color: { type: string, value: red } } and don't rewrite + // user will end up with { color: color, type: string, value: red } + // ... so keyForKey should probably be reserved for things which are definitely primitives + return; + } + + Map yamlMap = MutableMap.copyOf(getOutputYamlMap()); + yamlMap.remove(keyForKey); + + YomlUtils.removeDefaults(defaults, yamlMap); + + Object newValue = null; + + if (yamlMap.size()==1) { + // if after removing the keyForKey and defaults there is just one entry, see if we can abbreviate further + // eg using keyForPrimitiveValue + // generally we can do this if the remaining key equals the explicit key for that category, + // or if there is no key for that category but the any-value key is set and matches the remaining key + // and merging is not disallowed + Object remainingKey = yamlMap.keySet().iterator().next(); + if (remainingKey!=null) { + Object remainingObject = yamlMap.values().iterator().next(); + + if (remainingObject instanceof Map) { + // cannot promote if merge is true + if (!Boolean.TRUE.equals(mergeWithMapValue)) { + if (remainingKey.equals(keyForMapValue)) { + newValue = remainingObject; + } else if (Strings.isBlank(keyForMapValue) && remainingKey.equals(keyForAnyValue) && Boolean.FALSE.equals(mergeWithMapValue)) { + newValue = remainingObject; + } + } + } else if (remainingObject instanceof Iterable) { + if (remainingKey.equals(keyForListValue)) { + newValue = remainingObject; + } else if (Strings.isBlank(keyForListValue) && remainingKey.equals(keyForAnyValue) && Boolean.FALSE.equals(mergeWithMapValue)) { + newValue = remainingObject; + } + } else if (isJsonPrimitiveObject(remainingObject)) { + if (remainingKey.equals(keyForPrimitiveValue)) { + newValue = remainingObject; + } else if (Strings.isBlank(keyForPrimitiveValue) && remainingKey.equals(keyForAnyValue) && Boolean.FALSE.equals(mergeWithMapValue)) { + newValue = remainingObject; + } + } + } + } + + // if we couldn't simplify above, we might still be able to proceed, if: + // * merging is forced; OR + // * merging isn't disallowed, and + // * there is no keyForMapValue, and + // * it wouldn't cause a collision + // (if keyForMapValue is in effect, it will steal what we want to merge; + // but keyForAnyValue will only be used if merge is set true) + if (newValue==null && Boolean.TRUE.equals(isForMerging(yamlMap))) { + newValue = yamlMap; + } + if (newValue==null) return; // this serializer was cancelled, it doesn't apply + + context.setYamlObject(MutableMap.of(newKey, newValue)); + context.phaseRestart(); + } + } + + public boolean allowInMode(SingletonMapMode currentMode) { + return getAllowedModes().contains(currentMode); + } + + protected Collection getAllowedModes() { + MutableSet modes = MutableSet.of(); + modes.addIfNotNull(onlyInMode); + modes.putAll(onlyInModes); + if (modes.isEmpty()) return Arrays.asList(SingletonMapMode.values()); + return modes; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/DefaultMapValuesSerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/DefaultMapValuesSerializer.java new file mode 100644 index 0000000000..0d8d9f8877 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/DefaultMapValuesSerializer.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; +import org.apache.brooklyn.util.yoml.annotations.YomlDefaultMapValues; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +@YomlAllFieldsTopLevel +@YomlConfigMapConstructor("defaults") +@Alias("default-map-values") +public class DefaultMapValuesSerializer extends YomlSerializerComposition { + + DefaultMapValuesSerializer() { } + + public DefaultMapValuesSerializer(YomlDefaultMapValues ann) { + this(YomlUtils.extractDefaultMap(ann.value())); + } + + public DefaultMapValuesSerializer(Map defaults) { + super(); + this.defaults = defaults; + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + Map defaults; + + public class Worker extends YomlSerializerWorker { + public void read() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + // runs before type instantiated + if (hasJavaObject()) return; + if (!isYamlMap()) return; + + if (getYamlKeysOnBlackboardInitializedFromYamlMap().addDefaults(defaults)==0) return; + + context.phaseRestart(); + } + + public void write() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + if (!isYamlMap()) return; + + if (YomlUtils.removeDefaults(defaults, getOutputYamlMap())==0) return; + + context.phaseRestart(); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/FieldsInMapUnderFields.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/FieldsInMapUnderFields.java new file mode 100644 index 0000000000..9a5ce805bc --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/FieldsInMapUnderFields.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContext.StandardPhases; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FieldsInMapUnderFields extends YomlSerializerComposition { + + private static final Logger log = LoggerFactory.getLogger(FieldsInMapUnderFields.class); + + public static String KEY_NAME_FOR_MAP_OF_FIELD_VALUES = "fields"; + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + protected String getKeyNameForMapOfGeneralValues() { + return KEY_NAME_FOR_MAP_OF_FIELD_VALUES; + } + + protected String getExpectedPhaseRead() { + return YomlContext.StandardPhases.HANDLING_FIELDS; + } + + public class Worker extends YomlSerializerWorker { + + protected boolean setKeyValueForJavaObjectOnRead(String key, Object value, String optionalTypeConstraint) + throws IllegalAccessException { + Maybe ffm = Reflections.findFieldMaybe(getJavaObject().getClass(), key); + if (ffm.isAbsentOrNull()) { + // just skip (could throw, but leave it in case something else recognises it) + return false; + } else { + Field ff = ffm.get(); + if (Modifier.isStatic(ff.getModifiers())) { + // as above + return false; + } else { + String fieldType = getFieldTypeName(ff, optionalTypeConstraint); + Object v2 = converter.read( ((YomlContextForRead)context).subpath("/"+key, value, fieldType) ); + + if (isDeferredValue(v2)) { + Maybe coerced = config.getCoercer().tryCoerce(v2, ff.getType()); + if (coerced.isAbsent()) { + // couldn't coerce or resolve, and this is needed for fields of course + throw new YomlException("Cannot interpret or coerce '"+v2+"' as "+fieldType+" for field "+ff.getName(), + Maybe.getException(coerced)); + } + v2 = coerced.get(); + } + + ff.setAccessible(true); + ff.set(getJavaObject(), v2); + return true; + } + } + } + + protected String getFieldTypeName(Field ff, String optionalTypeConstraint) { + return merge(ff, ff.getType(), YomlUtils.getFieldTypeName(ff, config), optionalTypeConstraint); + } + + protected String merge(Object elementForError, Class localTypeJ, String localTypeName, String optionalTypeConstraint) { + String fieldType; + if (optionalTypeConstraint!=null) { + if (localTypeJ!=null && !Object.class.equals(localTypeJ)) { + throw new YomlException("Cannot apply inferred type "+optionalTypeConstraint+" for non-Object field "+elementForError, context); + // is there a "combineTypes(fieldType, optionalTypeConstraint)" method? + // that would let us weaken the above + } + fieldType = optionalTypeConstraint; + } else { + fieldType = localTypeName; + } + return fieldType; + } + + protected boolean shouldHaveJavaObject() { return true; } + + public void read() { + if (!context.isPhase(getExpectedPhaseRead())) return; + if (hasJavaObject() != shouldHaveJavaObject()) return; + + @SuppressWarnings("unchecked") + // written by the individual TopLevelFieldSerializers + Map fields = peekFromYamlKeysOnBlackboardRemaining(getKeyNameForMapOfGeneralValues(), Map.class).orNull(); + if (fields==null) return; + + MutableMap initialFields = MutableMap.copyOf(fields); + + List deferred = MutableList.of(); + boolean changed = false; + for (Object fo: initialFields.keySet()) { + String f = (String)fo; + if (TypeFromOtherFieldBlackboard.get(blackboard).getTypeConstraintField(f)!=null) { + deferred.add(f); + continue; + } + Object v = ((Map)fields).get(f); + try { + if (setKeyValueForJavaObjectOnRead(f, v, null)) { + ((Map)fields).remove(f); + changed = true; + } + } catch (Exception e) { throw Exceptions.propagate(e); } + } + + // defer for objects whose types come from another field + for (String f: deferred) { + Object typeO; + String tf = TypeFromOtherFieldBlackboard.get(blackboard).getTypeConstraintField(f); + boolean isTypeFieldReal = TypeFromOtherFieldBlackboard.get(blackboard).isTypeConstraintFieldReal(f); + + if (isTypeFieldReal) { + typeO = initialFields.get(tf); + } else { + typeO = peekFromYamlKeysOnBlackboardRemaining(tf, Object.class).orNull(); + } + if (typeO!=null && !(typeO instanceof String)) { + throw new YomlException("Wrong type of value '"+typeO+"' inferred as type of '"+f+"' on "+getJavaObject(), context); + } + String type = (String)typeO; + + Object v = ((Map)fields).get(f); + try { + if (setKeyValueForJavaObjectOnRead(f, v, type)) { + ((Map)fields).remove(f); + if (type!=null && !isTypeFieldReal) { + removeFromYamlKeysOnBlackboardRemaining(tf); + } + changed = true; + } + } catch (Exception e) { throw Exceptions.propagate(e); } + } + + if (((Map)fields).isEmpty()) { + removeFromYamlKeysOnBlackboardRemaining(getKeyNameForMapOfGeneralValues()); + } + if (changed) { + // restart (there is normally nothing after this so could equally continue with rerun) + context.phaseRestart(); + } + } + + public void write() { + if (!context.isPhase(StandardPhases.HANDLING_FIELDS)) return; + if (!isYamlMap()) return; + + // should have been set by FieldsInMapUnderFields if we are to run + if (getFromOutputYamlMap(getKeyNameForMapOfGeneralValues(), Map.class).isPresent()) return; + + Map fields = writePrepareGeneralMap(); + if (fields!=null && !fields.isEmpty()) { + setInOutputYamlMap(getKeyNameForMapOfGeneralValues(), fields); + // restart in case a serializer moves the `fields` map somewhere else + context.phaseRestart(); + } + } + + protected Map writePrepareGeneralMap() { + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard); + if (fib==null || fib.fieldsToWriteFromJava==null || fib.fieldsToWriteFromJava.isEmpty()) return null; + Map fields = MutableMap.of(); + + for (String f: MutableList.copyOf(fib.fieldsToWriteFromJava)) { + Maybe v = Reflections.getFieldValueMaybe(getJavaObject(), f); + if (v.isPresent()) { + fib.fieldsToWriteFromJava.remove(f); + if (v.get()==null) { + // silently drop null fields + } else { + Field ff = Reflections.findFieldMaybe(getJavaObject().getClass(), f).get(); + + String fieldType = getFieldTypeName(ff, null); + + // can we record additional information about the type in the yaml? + String tf = TypeFromOtherFieldBlackboard.get(blackboard).getTypeConstraintField(f); + if (tf!=null) { + if (!Object.class.equals(ff.getType())) { + // currently we only support smart types if the base type is object; + // see getFieldTypeName + + } else { + if (!TypeFromOtherFieldBlackboard.get(blackboard).isTypeConstraintFieldReal(f)) { + String realType = config.getTypeRegistry().getTypeName(v.get()); + fieldType = realType; + // for non-real, just write the pseudo-type-field at root + getOutputYamlMap().put(tf, realType); + + } else { + Maybe rt = Reflections.getFieldValueMaybe(getJavaObject(), tf); + if (rt.isPresentAndNonNull()) { + if (rt.get() instanceof String) { + fieldType = (String) rt.get(); + } else { + throw new YomlException("Cannot use type information from "+tf+" for "+f+" as it is "+rt.get(), context); + } + } + } + } + } + + Object v2 = converter.write( ((YomlContextForWrite)context).subpath("/"+f, v.get(), fieldType) ); + fields.put(f, v2); + } + } + } + if (log.isTraceEnabled()) { + log.trace(this+": built fields map "+fields); + } + return fields; + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeEnum.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeEnum.java new file mode 100644 index 0000000000..8d2a661348 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeEnum.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +public class InstantiateTypeEnum extends YomlSerializerComposition { + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends InstantiateTypeWorkerAbstract { + + public void read() { + if (!canDoRead()) return; + + Class type = null; + boolean fromMap = false; + Maybe value = Maybe.absent(); + + if (getExpectedTypeJava()!=null) { + if (!getExpectedTypeJava().isEnum()) return; + + value = Maybe.of(getYamlObject()); + if (!isJsonPrimitiveObject(value.get())) { + // warn, but try { type: .., value: ... } syntax + warn("Enum of expected type "+getExpectedTypeJava()+" cannot be created from '"+value.get()+"'"); + + } else { + type = getExpectedTypeJava(); + } + } + + if (type==null) { + String typeName = readingTypeFromFieldOrExpected(); + if (typeName==null) return; + type = config.getTypeRegistry().getJavaTypeMaybe(typeName, context).orNull(); + // swallow exception in this context, it isn't meant for enum to resolve + if (type==null || !type.isEnum()) return; + value = readingValueFromTypeValueMap(); + if (value.isAbsent()) { + warn("No value declared for enum "+type); + return; + } + if (!isJsonPrimitiveObject(value.get())) { + warn("Enum of type "+getExpectedTypeJava()+" cannot be created from '"+value.get()+"'"); + return; + } + + fromMap = true; + } + + Maybe enumValue = tryCoerceAndNoteError(value.get(), type); + if (enumValue.isAbsent()) return; + + storeReadObjectAndAdvance(enumValue.get(), false); + if (fromMap) removeTypeAndValueKeys(); + } + + public void write() { + if (!canDoWrite()) return; + if (!getJavaObject().getClass().isEnum()) return; + + boolean wrap = true; + if (getExpectedTypeJava()!=null) { + if (!getExpectedTypeJava().isEnum()) return; + wrap = false; + } + + Object result = ((Enum)getJavaObject()).name(); + + if (wrap) { + String typeName = config.getTypeRegistry().getTypeName(getJavaObject()); + if (addSerializersForDiscoveredRealType(typeName, true)) { + // if new serializers, bail out and we'll re-run + context.phaseRestart(); + return; + } + + result = writingMapWithTypeAndLiteralValue(typeName, result); + } + + context.phaseInsert(YomlContext.StandardPhases.MANIPULATING); + storeWriteObjectAndAdvance(result); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistry.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistry.java new file mode 100644 index 0000000000..fa45fbfc87 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistry.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +public class InstantiateTypeFromRegistry extends YomlSerializerComposition { + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends InstantiateTypeWorkerAbstract { + + public void read() { + if (!canDoRead()) return; + + String type = null; + if (getYamlObject() instanceof CharSequence) { + // string on its own interpreted as a type + type = Strings.toString(getYamlObject()); + } else { + // otherwise should be map + if (!isYamlMap()) return; + + type = readingTypeFromFieldOrExpected(); + } + + if (type==null) return; + + if (!readType(type, true)) return; + + if (isYamlMap()) { + removeFromYamlKeysOnBlackboardRemaining("type"); + } + } + + protected boolean readType(String type, boolean isRoot) { + if (addSerializersForDiscoveredRealType(type, isRoot)) { + // added new serializers so restart phase + // in case another serializer wants to create it + context.phaseRestart(); + return false; + } + + Maybe resultM = config.getTypeRegistry().newInstanceMaybe(type, Yoml.newInstance(config), context); + if (resultM.isAbsent()) { + String message = "Unable to create type '"+type+"'"; + RuntimeException exc = null; + + Maybe> jt = config.getTypeRegistry().getJavaTypeMaybe(type, context); + if (jt.isAbsent()) { + exc = ((Maybe.Absent)jt).getException(); + } else { + exc = ((Maybe.Absent)resultM).getException(); + } + warn(new IllegalStateException(message, exc)); + return false; + } + + storeReadObjectAndAdvance(resultM.get(), true); + return true; + } + + public void write() { + if (!canDoWrite()) return; + + if (Reflections.hasSpecialSerializationMethods(getJavaObject().getClass())) { + warn("Cannot write "+getJavaObject().getClass()+" using default strategy as it has custom serializaton methods"); + return; + } + + // common primitives and maps/lists will have been handled + + // (osgi syntax isn't supported, because we expect items to be in the registry) + + String typeName = getJavaObject().getClass().equals(getExpectedTypeJava()) ? null : config.getTypeRegistry().getTypeName(getJavaObject()); + if (addSerializersForDiscoveredRealType(typeName, true)) { + // if new serializers, bail out and we'll re-run + context.phaseRestart(); + return; + } + + MutableMap map = writingMapWithType( + // explicitly write the type (unless it is the expected one) + typeName); + + writingPopulateBlackboard(); + writingInsertPhases(); + + storeWriteObjectAndAdvance(map); + return; + } + + protected void writingInsertPhases() { + context.phaseInsert(YomlContext.StandardPhases.HANDLING_FIELDS, YomlContext.StandardPhases.MANIPULATING); + } + + protected void writingPopulateBlackboard() { + // collect fields + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard); + fib.fieldsToWriteFromJava.addAll(YomlUtils.getAllNonTransientNonStaticFieldNamesUntyped(getJavaObject().getClass(), getJavaObject())); + } + + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistryUsingConfigMap.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistryUsingConfigMap.java new file mode 100644 index 0000000000..5f608b1fe4 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeFromRegistryUsingConfigMap.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.config.ConfigInheritance; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; + +import com.google.common.base.Preconditions; + +@Alias("config-map-constructor") +/** Special instantiator for when the class's constructor takes a Map of config */ +public class InstantiateTypeFromRegistryUsingConfigMap extends InstantiateTypeFromRegistry { + + // for config keys we need an extra "manipulating" phase + public static final String PHASE_INSTANTIATE_TYPE_DEFERRED = "handling-type-deferred-after-config"; + + protected String keyNameForConfigWhenSerialized = null; + protected String fieldNameForConfigInJava = null; + boolean staticKeysRequired; + + // don't currently fully support inferring setup from annotations; we need the field above. + // easily could automate with a YomlConfigMap annotation - but for now make it explicit + // (for now this field can be used to load explicit config keys, if the field name is supplied) + boolean inferByScanning = false; + + public static Factory newFactoryIgnoringInheritance() { + return new Factory(); + } + + public static class Factory { + + protected Factory() {} + + /** creates a set of serializers handling config for any type, with the given field/key combination; + * the given field will be checked at serialization time to determine whether this is applicable */ + public Set newConfigKeyClassScanningSerializers( + String fieldNameForConfigInJava, String keyNameForConfigWhenSerialized, boolean requireStaticKeys) { + + return findSerializers(null, + fieldNameForConfigInJava, keyNameForConfigWhenSerialized, + false, requireStaticKeys); + } + + /** creates a set of serializers handling config for the given type, for use in a type-specific serialization, + * permitting multiple field/key combos; if the given field is not found, the pair is excluded here */ + public Set newConfigKeySerializersForType( + Class type, + String fieldNameForConfigInJava, String keyNameForConfigWhenSerialized, + boolean validateAheadOfTime, boolean requireStaticKeys) { + return findSerializers(type, fieldNameForConfigInJava, keyNameForConfigWhenSerialized, validateAheadOfTime, requireStaticKeys); + } + + protected Set findSerializers( + Class type, + String fieldNameForConfigInJava, String keyNameForConfigWhenSerialized, + boolean validateAheadOfTime, boolean requireStaticKeys) { + MutableSet result = MutableSet.of(); + if (fieldNameForConfigInJava==null) return result; + InstantiateTypeFromRegistryUsingConfigMap instantiator = newInstance(); + instantiator.fieldNameForConfigInJava = fieldNameForConfigInJava; + if (validateAheadOfTime) { + Preconditions.checkArgument(instantiator.isValidConfigFieldOrBlankSoWeCanRead(type), "Missing config field "+fieldNameForConfigInJava+" in "+type); + instantiator.findConstructorMaybe(type).get(); + } + instantiator.keyNameForConfigWhenSerialized = keyNameForConfigWhenSerialized; + instantiator.staticKeysRequired = false; + + if (type!=null) { + instantiator.inferByScanning = false; + result.addAll(TopLevelConfigKeySerializer.findConfigKeySerializers(keyNameForConfigWhenSerialized, type)); + } else { + instantiator.inferByScanning = true; + } + + result.add(new ConfigInMapUnderConfigSerializer(keyNameForConfigWhenSerialized)); + result.add(instantiator); + + return result; + } + + protected InstantiateTypeFromRegistryUsingConfigMap newInstance() { + return new InstantiateTypeFromRegistryUsingConfigMap(); + } + } + + protected InstantiateTypeFromRegistryUsingConfigMap() {} + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + class Worker extends InstantiateTypeFromRegistry.Worker { + + @Override + public void read() { + if (context.isPhase(PHASE_INSTANTIATE_TYPE_DEFERRED)) { + readFinallyCreate(); + } else { + super.read(); + } + } + + @Override + protected boolean readType(String type, boolean isRoot) { + Class clazz = config.getTypeRegistry().getJavaTypeMaybe(type, context).orNull(); + if (!isConfigurable(clazz)) return false; + + // prepare blackboard, annotations, then do handling_config + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.create(blackboard, keyNameForConfigWhenSerialized); + + fib.typeNameFromReadToConstructJavaLater = type; + fib.typeFromReadToConstructJavaLater = clazz; + fib.fieldsFromReadToConstructJava = MutableMap.of(); + + addSerializersForDiscoveredRealType(type, isRoot); + addExtraTypeSerializers(clazz); + + context.phaseInsert(YomlContext.StandardPhases.MANIPULATING, PHASE_INSTANTIATE_TYPE_DEFERRED); + context.phaseAdvance(); + return true; + } + + protected void addExtraTypeSerializers(Class clazz) { + if (!inferByScanning) return; + + // prevent multiple additions + if (!putLabelOnBlackboard("extra-type-serializers="+clazz, true)) return; + + SerializersOnBlackboard.get(blackboard).addInstantiatedTypeSerializers( + TopLevelConfigKeySerializer.findConfigKeySerializers(keyNameForConfigWhenSerialized, clazz) ); + } + + protected TopLevelFieldsBlackboard getTopLevelFieldsBlackboard() { + // keys recorded here by the individual serializers + return TopLevelFieldsBlackboard.get(blackboard, keyNameForConfigWhenSerialized); + } + + protected void readFinallyCreate() { + if (hasJavaObject()) return; + + // this is running in a later phase, after the brooklyn.config map has been set up + // instantiate with special constructor + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard, keyNameForConfigWhenSerialized); + Class type = fib.typeFromReadToConstructJavaLater; + if (type==null) return; + + Preconditions.checkNotNull(keyNameForConfigWhenSerialized); + + YomlContextForRead constructionContext = ((YomlContextForRead)context).constructionInstruction( + newConstructor(type, getTopLevelFieldsBlackboard().getConfigKeys(), MutableMap.copyOf(fib.fieldsFromReadToConstructJava), + context.getConstructionInstruction()) ); + + Maybe resultM = config.getTypeRegistry().newInstanceMaybe(fib.typeNameFromReadToConstructJavaLater, Yoml.newInstance(config), constructionContext); + + if (resultM.isAbsent()) { + warn(new IllegalStateException("Unable to create type '"+type+"'", ((Maybe.Absent)resultM).getException())); + return; + } + + fib.fieldsFromReadToConstructJava.clear(); + storeReadObjectAndAdvance(resultM.get(), true); + } + + @Override + protected boolean canDoWrite() { + if (!super.canDoWrite()) return false; + if (!isConfigurable(getJavaObject().getClass())) return false; + if (!isValidConfigFieldSoWeCanWrite()) return false; + + return true; + } + + @Override + protected void writingPopulateBlackboard() { + super.writingPopulateBlackboard(); + + try { + String configMapKeyName = fieldNameForConfigInJava; + if (configMapKeyName==null) { + if (!inferByScanning) { + throw new IllegalStateException("no config key name set and not allowed to infer; " + + "this serializer should only be used when the config key name is specified"); + } else { + // optionally: we could support annotation on the type to learn the key name; + // but without that we just write as fields + throw new UnsupportedOperationException("config key name must be set explicitly"); + } + } + // write clues for ConfigInMapUnder... + + JavaFieldsOnBlackboard fib = JavaFieldsOnBlackboard.peek(blackboard); + Field f = Reflections.findFieldMaybe(getJavaObject().getClass(), fieldNameForConfigInJava).get(); + f.setAccessible(true); + Map configMap = getRawConfigMap(f, getJavaObject()); + if (configMap!=null) { + fib.configToWriteFromJava = MutableMap.copyOf(configMap); + } + + // suppress wherever the config is stored + fib.fieldsToWriteFromJava.remove(configMapKeyName); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + warn(new IllegalStateException("Unable to retieve config map in "+getJavaObject(), e)); + return; + } + + addExtraTypeSerializers(getJavaObject().getClass()); + } + + /** see {@link #isValidConfigFieldOrBlankSoWeCanRead(Class)}; cannot write via this if field blank + * as we cannot reverse engineer config keys from fields */ + protected boolean isValidConfigFieldSoWeCanWrite() { + if (Strings.isBlank(fieldNameForConfigInJava)) return false; + return findFieldMaybe(getJavaObject().getClass()).isPresent(); + } + + protected void writingInsertPhases() { + super.writingInsertPhases(); + // for configs, we need to do this to get type info (and preferred aliases) + context.phaseInsert(TopLevelFieldSerializer.Worker.PREPARING_TOP_LEVEL_FIELDS); + } + + } + + @SuppressWarnings("unchecked") + protected Map getRawConfigMap(Field f, Object obj) throws IllegalAccessException { + return (Map)f.get(obj); + } + + /** configurable if it has a map constructor and at least one public static config key */ + protected boolean isConfigurable(Class type) { + if (type==null) return false; + if (findConstructorMaybe(type).isAbsent()) return false; + if (!isValidConfigFieldOrBlankSoWeCanRead(type)) return false; + if (staticKeysRequired && TopLevelConfigKeySerializer.findConfigKeys(type).isEmpty()) return false; + return true; + } + + /** can be blank if reads are supported to a constructor but no config map field is used within + * the class, ie fields for each config value are populated on construction */ + protected boolean isValidConfigFieldOrBlankSoWeCanRead(Class type) { + if ("".equals(fieldNameForConfigInJava)) return true; + return findFieldMaybe(type).isPresent(); + } + + protected Maybe findFieldMaybe(Class type) { + Maybe f = Reflections.findFieldMaybe(type, fieldNameForConfigInJava); + if (f.isPresent() && !Map.class.isAssignableFrom(f.get().getType())) f = Maybe.absent(); + return f; + } + + protected Maybe findConstructorMaybe(Class type) { + return Reflections.findConstructorExactMaybe(type, Map.class); + } + + /** + * creates an instruction for working with a single-argument constructor which takes a simple map of + * config values. + *

+ * this ignores inheritance since within this project specific ConfigInheritanceContext + * and {@link ConfigInheritance} strategies are not available. + *

+ * callers will have already invoked {@link #findConstructorMaybe(Class)} so implementations can + * assume the constructor exists. subclassers should ensure that {@link #findConstructorMaybe(Class)} is + * also updated if required. + */ + protected ConstructionInstruction newConstructor(Class type, Map> keysByAlias, + Map fieldsFromReadToConstructJava, ConstructionInstruction optionalOuter) { + return ConstructionInstructions.Factory.newUsingConstructorWithArgs(type, + MutableList.of(fieldsFromReadToConstructJava), optionalOuter); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeList.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeList.java new file mode 100644 index 0000000000..be240be969 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeList.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.internal.YomlContext.StandardPhases; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.GenericsParse; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.JsonMarker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Objects; +import com.google.common.collect.Iterables; + +/* + * if expecting a coll + * and it's not a coll + * if primitive, try as type + * if map, new conversion phase + * and it is a coll + * instantiate then go through coll + * if not expecting a coll + * and it's not a coll + * do nothing + * and it is a coll + * try conversion to map + * + * repeat with value + */ +public class InstantiateTypeList extends YomlSerializerComposition { + + public static final String MANIPULATING_FROM_LIST = "manipulating-from-list"; + public static final String MANIPULATING_TO_LIST = "manipulating-to-list"; + + private static final Logger log = LoggerFactory.getLogger(InstantiateTypeList.class); + + private static final String LIST = YomlUtils.TYPE_LIST; + private static final String SET = YomlUtils.TYPE_SET; + + @SuppressWarnings("rawtypes") + Map> typeAliases = MutableMap.>of( + LIST, MutableList.class, + SET, MutableSet.class + ); + + @SuppressWarnings("rawtypes") + Map, String> typesAliased = MutableMap.,String>of( + MutableList.class, LIST, + ArrayList.class, LIST, + List.class, LIST, + MutableSet.class, SET, + LinkedHashSet.class, SET, + Set.class, SET + ); + Map typesAliasedByName = MutableMap.of( + Arrays.class.getCanonicalName()+"$ArrayList", LIST //Arrays.ArrayList is changed to default + ); + + // any other types we allow? (expect this to be populated by trial and error) + @SuppressWarnings("rawtypes") + Set> typesAllowed = MutableSet.>of( + ); + Set typesAllowedByName = MutableSet.of( + ); + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends InstantiateTypeWorkerAbstract { + + String genericSubType = null; + Class expectedJavaType; + + public void read() { + if (!canDoRead()) return; + Object yo = getYamlObject(); + expectedJavaType = getExpectedTypeJava(); + + if (context.getExpectedType()!=null && !parseExpectedTypeAndDetermineIfNoBadProblems(context.getExpectedType())) return; + + if (expectedJavaType!=null && !Iterable.class.isAssignableFrom(expectedJavaType)) { + // expecting something other than a collection + if (!(yo instanceof Iterable)) { + // and not given a collection -- we do nothing + return; + } else { + // but we have a collection + // spawn manipulate-from-list phase + if (!context.seenPhase(MANIPULATING_FROM_LIST)) { + context.phaseInsert(MANIPULATING_FROM_LIST, YomlContext.StandardPhases.HANDLING_TYPE); + context.phaseAdvance(); + } + return; + } + } else if (!(yo instanceof Iterable)) { + // no expectation or expecting a collection, but not given a collection + if (yo instanceof Map) { + String type = readingTypeFromFieldOrExpected(); + Object value = readingValueFromTypeValueMap().orNull(); + Class oldExpectedType = expectedJavaType; + + // get any new generic type set - slightly messy + if (!parseExpectedTypeAndDetermineIfNoBadProblems(type)) return; + Maybe> javaTypeM = config.getTypeRegistry().getJavaTypeMaybe(type, context); + Class javaType; + if (javaTypeM.isPresent()) javaType = javaTypeM.get(); + else { + // the expected java type is now based on inference from `type` + // so if it wasn't recognised we'll bail out below + javaType = expectedJavaType; + } + + expectedJavaType = oldExpectedType; + + if (javaType==null || value==null || !Collection.class.isAssignableFrom(javaType) || !Iterable.class.isInstance(value)) { + // we don't apply, at least not yet, but may need to manipulate *to* a list + } else { + // looks like a list in a type-value map + Object jo = newInstance(expectedJavaType, type); + if (jo==null) return; + + context.setJavaObject(jo); + + readIterableInto((Collection)jo, (Iterable)value); + context.phaseAdvance(); + removeTypeAndValueKeys(); + return; + } + } + if (expectedJavaType!=null) { + // collection definitely expected but not received, schedule manipulation phase + if (!context.seenPhase(MANIPULATING_TO_LIST)) { + // and add converters for the generic subtype + SerializersOnBlackboard.get(blackboard).addExpectedTypeSerializers( + config.getTypeRegistry().getSerializersForType(genericSubType, context.subpath("<>", null, null)) ); + context.phaseInsert(MANIPULATING_TO_LIST, YomlContext.StandardPhases.HANDLING_TYPE); + context.phaseAdvance(); + } else { + warn("Unable to manipulate input to be a list when a list is expected"); + } + return; + } + // otherwise standard InstantiateType will try it + return; + } else { + // given a collection, when expecting a collection or no expectation -- read as list + + if (!context.seenPhase(MANIPULATING_TO_LIST)) { + // first apply manipulations, + // and add converters for the generic subtype + SerializersOnBlackboard.get(blackboard).addExpectedTypeSerializers( + config.getTypeRegistry().getSerializersForType(genericSubType, context.subpath("<>", null, null)) ); + context.phaseInsert(MANIPULATING_TO_LIST, StandardPhases.MANIPULATING, YomlContext.StandardPhases.HANDLING_TYPE); + context.phaseAdvance(); + return; + } + + Object jo; + if (hasJavaObject()) { + // populating previous java object + jo = context.getJavaObject(); + + } else { + jo = newInstance(expectedJavaType, null); + if (jo==null) return; + + context.setJavaObject(jo); + } + + readIterableInto((Collection)jo, (Iterable)yo); + context.phaseAdvance(); + } + } + + protected boolean parseExpectedTypeAndDetermineIfNoBadProblems(String type) { + if (isJsonMarkerType(type)) { + genericSubType = YomlUtils.TYPE_JSON; + } else { + GenericsParse gp = new GenericsParse(type); + if (gp.warning!=null) { + warn(gp.warning); + return false; + } + if (gp.isGeneric()) { + if (gp.subTypeCount()!=1) { + // not a list + return false; + } + genericSubType = Iterables.getOnlyElement(gp.subTypes); + } + if (expectedJavaType==null) { + expectedJavaType = typeAliases.get(gp.baseType); + } + } + return true; + } + + private Object newInstance(Class javaType, String explicitTypeName) { + if (explicitTypeName!=null) { + GenericsParse gp = new GenericsParse(explicitTypeName); + if (gp.warning!=null) { + warn(gp.warning+" (creating "+javaType+")"); + return null; + } + + Class locallyWantedType = typeAliases.get(gp.baseType); + + if (locallyWantedType==null) { + // rely on type registry + return config.getTypeRegistry().newInstance(explicitTypeName, Yoml.newInstance(config)); + } + + // create it ourselves, but first assert it matches expected + if (javaType!=null) { + if (locallyWantedType.isAssignableFrom(javaType)) { + // prefer the java type + } else if (javaType.isAssignableFrom(locallyWantedType)) { + // prefer locally wanted + javaType = locallyWantedType; + } + } else { + javaType = locallyWantedType; + } + + // and set the subtype + if (gp.subTypeCount()==1) { + String subType = Iterables.getOnlyElement(gp.subTypes); + if (genericSubType!=null && !genericSubType.equals(subType)) { + log.debug("Got different generic subtype, expected "+context.getExpectedType()+" but declared "+explicitTypeName+"; preferring declared"); + } + genericSubType = subType; + } + } + + Class concreteJavaType = null; + if (javaType==null || javaType.isInterface() || Modifier.isAbstract(javaType.getModifiers())) { + // take first from default types that matches + for (Class candidate : typeAliases.values()) { + if (javaType==null || javaType.isAssignableFrom(candidate)) { + concreteJavaType = candidate; + break; + } + } + if (concreteJavaType==null) { + // fallback, if given interface create as list + warn("No information to instantiate list "+javaType); + return null; + } + + } else { + concreteJavaType = javaType; + } + if (!Collection.class.isAssignableFrom(concreteJavaType)) { + warn("No information to add items to list "+concreteJavaType); + return null; + } + + try { + return concreteJavaType.newInstance(); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + + protected void readIterableInto(Collection joq, Iterable yo) { + // go through collection, creating from children + + @SuppressWarnings("unchecked") + Collection jo = (Collection) joq; + int index = 0; + + for (Object yi: yo) { + jo.add(converter.read( ((YomlContextForRead)context).subpath("["+index+"]", yi, genericSubType) )); + index++; + } + } + + public void write() { + if (!canDoWrite()) return; + + boolean isPureJson = YomlUtils.JsonMarker.isPureJson(getJavaObject()); + + // if expecting json then: + if (isJsonMarkerTypeExpected()) { + if (!isPureJson) { + warn("Cannot write "+getJavaObject()+" as pure JSON"); + return; + } + @SuppressWarnings("unchecked") + Collection l = Reflections.invokeConstructorFromArgsIncludingPrivate(typeAliases.values().iterator().next()).get(); + Iterables.addAll(l, (Iterable)getJavaObject()); + storeWriteObjectAndAdvance(l); + return; + } + + Class expectedJavaType = getExpectedTypeJava(); + GenericsParse gp = new GenericsParse(context.getExpectedType()); + if (gp.warning!=null) { + warn(gp.warning); + return; + } + if (gp.isGeneric()) { + if (gp.subTypeCount()!=1) { + // not a list + return; + } + genericSubType = Iterables.getOnlyElement(gp.subTypes); + } + Class newExpectedType = typeAliases.get(gp.baseType); + if (newExpectedType!=null && (expectedJavaType==null || expectedJavaType.isAssignableFrom(newExpectedType))) { + expectedJavaType = newExpectedType; + } + String expectedJavaTypeName = typesAliased.get(expectedJavaType); + if (expectedJavaTypeName==null && expectedJavaType!=null) expectedJavaTypeName = typesAliasedByName.get(expectedJavaType.getName()); + + if (expectedJavaTypeName!=null) expectedJavaType = typeAliases.get(expectedJavaTypeName); + else expectedJavaTypeName = config.getTypeRegistry().getTypeNameOfClass(expectedJavaType); + + String actualTypeName = typesAliased.get(getJavaObject().getClass()); + if (actualTypeName==null) actualTypeName = typesAliasedByName.get(getJavaObject().getClass().getName()); + + boolean isBasicCollectionType = (actualTypeName!=null); + if (actualTypeName==null) actualTypeName = config.getTypeRegistry().getTypeName(getJavaObject()); + if (actualTypeName==null) return; + boolean isAllowedCollectionType = isBasicCollectionType || + typesAllowed.contains(getJavaObject().getClass()) || + typesAllowedByName.contains(getJavaObject().getClass().getName()); + if (!isAllowedCollectionType) return; + + Class reconstructedJavaType = typeAliases.get(actualTypeName); + if (reconstructedJavaType==null) reconstructedJavaType = getJavaObject().getClass(); + + Object result; + Collection list = MutableList.of(); + + boolean writeWithoutTypeInformation = Objects.equal(reconstructedJavaType, expectedJavaType); + if (!writeWithoutTypeInformation) { + @SuppressWarnings("rawtypes") + Class defaultCollectionType = typeAliases.isEmpty() ? null : typeAliases.values().iterator().next(); + if (Objects.equal(reconstructedJavaType, defaultCollectionType)) { + // actual type is the default - typically can omit saying the type + if (context.getExpectedType()==null) writeWithoutTypeInformation = true; + else if (expectedJavaType!=null && expectedJavaType.isAssignableFrom(defaultCollectionType)) writeWithoutTypeInformation = true; + else { + // possibly another problem -- expecting something different to default + // don't fret, just include the type specifically + // likely they're just expecting an explicit collection type other than our default + actualTypeName = config.getTypeRegistry().getTypeName(getJavaObject()); + } + } + } + if ((YomlUtils.TYPE_LIST.equals(actualTypeName) || (YomlUtils.TYPE_SET.equals(actualTypeName))) && genericSubType==null) { + if (JsonMarker.isPureJson(getJavaObject()) && !Iterables.isEmpty((Iterable)getJavaObject())) { + writeWithoutTypeInformation = false; + actualTypeName = actualTypeName+"<"+YomlUtils.TYPE_JSON+">"; + genericSubType = YomlUtils.TYPE_JSON; + } + } + + if (writeWithoutTypeInformation) { + // add directly if we are expecting this + result = list; + } else if (!isAllowedCollectionType) { + // not to be written with this serializer + return; + } else { + // need to include the type name + if (actualTypeName==null) return; + result = MutableMap.of("type", actualTypeName, "value", list); + } + + int index = 0; + for (Object ji: (Iterable)getJavaObject()) { + list.add(converter.write( ((YomlContextForWrite)context).subpath("/["+index+"]", ji, genericSubType) )); + index++; + } + + storeWriteObjectAndAdvance(result); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeMap.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeMap.java new file mode 100644 index 0000000000..c4bc13e105 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeMap.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Reflections; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.GenericsParse; + +import com.google.common.base.Objects; + + +public class InstantiateTypeMap extends YomlSerializerComposition { + + private static final String MAP = YomlUtils.TYPE_MAP; + + // these mapping class settings are slightly OTT but keeping for consistency with list and in case we have 'alias' + + @SuppressWarnings("rawtypes") + Map> typeAliases = MutableMap.>of( + MAP, MutableMap.class + ); + + @SuppressWarnings("rawtypes") + Map, String> typesAliased = MutableMap.,String>of( + MutableMap.class, MAP, + LinkedHashMap.class, MAP + ); + + @SuppressWarnings("rawtypes") + Set> typesAllowed = MutableSet.>of( + // TODO does anything fit this category? we serialize as a json map, including the type, and use xxx.put(...) to read in + ); + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends InstantiateTypeWorkerAbstract { + + String genericKeySubType = null, genericValueSubType = null; + Class expectedJavaType; + String expectedBaseTypeName; + + public void read() { + if (!canDoRead()) return; + Object yo = getYamlObject(); + + expectedJavaType = getExpectedTypeJava(); + if (context.getExpectedType()!=null && !parseExpectedTypeAndDetermineIfNoBadProblems(context.getExpectedType())) return; + + // if expected type is json then we just dump what we have / read what's there, end + // if expected type in the allows list or compatible with "map" and j.u.Map, look at that value + // else try read/write as { type: mapxxx, value: valuexxx } + // else end, it isn't for us + // then for value + // if all keys are primitives then write as map { keyxxx: valuexxx } + // else write as list of kv pairs - [ { key: keyxxx , value: valuexxx } ] + // (in both cases using generics if available) + + if (isJsonMarkerTypeExpected()) { + // json is pass-through + context.setJavaObject( context.getYamlObject() ); + context.phaseAdvance(); + YamlKeysOnBlackboard.getOrCreate(blackboard, null).clearRemaining(); + return; + } + + if (expectedJavaType!=null && !Map.class.isAssignableFrom(expectedJavaType)) return; // not a map expected + + if (isJsonPrimitiveObject(yo)) return; + + String actualBaseTypeName; + Class actualType; + Object value; + String alias = getAlias(expectedJavaType); + if (alias!=null || typesAllowed.contains(expectedJavaType)) { + // type would not have been written + actualBaseTypeName = expectedBaseTypeName; + value = getYamlObject(); + + } else { + // the type must have been made explicit + if (!isYamlMap()) return; + actualBaseTypeName = readingTypeFromFieldOrExpected(); + Maybe valueM = readingValueFromTypeValueMap(); + if (actualBaseTypeName==null && valueM.isAbsent()) return; + + Class oldExpectedJavaType = expectedJavaType; expectedJavaType = null; + String oldExpectedBaseTypeName = expectedBaseTypeName; expectedBaseTypeName = null; + parseExpectedTypeAndDetermineIfNoBadProblems(actualBaseTypeName); + // above will overwrite baseType + alias = getAlias(expectedJavaType); + actualType = expectedJavaType; + actualBaseTypeName = expectedBaseTypeName; + expectedJavaType = oldExpectedJavaType; + expectedBaseTypeName = oldExpectedBaseTypeName; + if (actualType==null) actualType = config.getTypeRegistry().getJavaTypeMaybe(actualBaseTypeName, context).orNull(); + if (actualType==null) return; //we don't recognise the type + if (!Map.class.isAssignableFrom(actualType)) return; //it's not a map + + value = valueM.get(); + } + + // create actualType, then populate from value + Map jo = null; + if (typeAliases.get(alias)!=null) { + jo = (Map) Reflections.invokeConstructorFromArgsIncludingPrivate(typeAliases.get(alias)).get(); + } else { + try { + jo = (Map) config.getTypeRegistry().newInstance(actualBaseTypeName, Yoml.newInstance(config)); + } catch (Exception e) { + throw new IllegalStateException("Cannot instantiate "+actualBaseTypeName, e); + } + } + @SuppressWarnings("unchecked") + Map jom = (Map)jo; + + if (value instanceof Iterable) { + int i=0; + for (Object o: (Iterable) value) { + String newPath = context.getJsonPath()+"["+i+"]"; + if (o instanceof Map) { + Map m = (Map) o; + if (m.isEmpty() || m.size()>2) { + warn("Invalid map-entry in list for map at "+newPath); + return; + } + Object ek1=null, ev1, ek2=null, ev2; + if (m.size()==1) { + ek2 = m.keySet().iterator().next(); + ev1 = m.values().iterator().next(); + } else { + if (!MutableSet.of("key", "value").containsAll(m.keySet())) { + warn("Invalid key-value entry in list for map at "+newPath); + return; + } + ek1 = m.get("key"); + ev1 = m.get("value"); + } + if (ek2==null) { + ek2 = converter.read( ((YomlContextForRead)context).subpath("/@key["+i+"]", ek1, genericKeySubType) ); + } + ev2 = converter.read( ((YomlContextForRead)context).subpath("/@value["+i+"]", ev1, genericValueSubType) ); + jom.put(ek2, ev2); + } else { + // must be an entry set, so invalid + // however at this point we are committed to it being a map, so throw + warn("Invalid non-map map-entry in list for map at "+newPath); + return; + } + i++; + } + + } else if (value instanceof Map) { + for (Map.Entry me: ((Map)value).entrySet()) { + Object v = converter.read( ((YomlContextForRead)context).subpath("/"+me.getKey(), me.getValue(), genericValueSubType) ); + jom.put(me.getKey(), v); + } + + } else { + // can't deal with primitive - but should have exited earlier + return; + } + + context.setJavaObject(jo); + context.phaseAdvance(); + YamlKeysOnBlackboard.getOrCreate(blackboard, null).clearRemaining(); + } + + private String getAlias(Class type) { + if (type==null || !Map.class.isAssignableFrom(type)) return null; + for (Class t: typesAliased.keySet()) { + if (type.isAssignableFrom(t)) { + return typesAliased.get(t); + } + } + return null; + } + + protected boolean parseExpectedTypeAndDetermineIfNoBadProblems(String type) { + if (isJsonMarkerType(type)) { + genericKeySubType = YomlUtils.TYPE_JSON; + genericValueSubType = YomlUtils.TYPE_JSON; + } else { + GenericsParse gp = new GenericsParse(type); + if (gp.warning!=null) { + warn(gp.warning); + return false; + } + if (gp.isGeneric()) { + if (gp.subTypeCount()!=2) { + // not a list + return false; + } + genericKeySubType = gp.subTypes.get(0); + genericValueSubType = gp.subTypes.get(1); + + if ("?".equals(genericKeySubType)) genericKeySubType = null; + if ("?".equals(genericValueSubType)) genericValueSubType = null; + } + if (expectedBaseTypeName==null) { + expectedBaseTypeName = gp.baseType; + } + if (expectedJavaType==null) { + expectedJavaType = typeAliases.get(gp.baseType); + } + } + return true; + } + + public void write() { + if (!canDoWrite()) return; + if (!(getJavaObject() instanceof Map)) return; + Map jo = (Map)getJavaObject(); + + expectedJavaType = getExpectedTypeJava(); + if (context.getExpectedType()!=null && !parseExpectedTypeAndDetermineIfNoBadProblems(context.getExpectedType())) return; + String expectedGenericKeySubType = genericKeySubType; + String expectedGenericValueSubType = genericValueSubType; + + boolean isPureJson = YomlUtils.JsonMarker.isPureJson(getJavaObject()); + + // if expecting json then + if (isJsonMarkerTypeExpected()) { + if (!isPureJson) { + warn("Cannot write "+getJavaObject()+" as pure JSON"); + return; + } + @SuppressWarnings("unchecked") + Map m = Reflections.invokeConstructorFromArgsIncludingPrivate(typesAliased.keySet().iterator().next()).get(); + m.putAll((Map)getJavaObject()); + storeWriteObjectAndAdvance(m); + return; + } + + // if expected type in the allows list or compatible with "map" and j.u.Map, look at that value + // else try read/write as { type: mapxxx, value: valuexxx } + // else end, it isn't for us + String alias = getAlias(getJavaObject().getClass()); + if (alias==null && !typesAllowed.contains(getJavaObject().getClass())) { + // actual type should not be written this way + return; + } + String aliasOfExpected = getAlias(expectedJavaType); + boolean writeWithoutTypeInformation; + if (alias!=null) writeWithoutTypeInformation = alias.equals(aliasOfExpected); + else writeWithoutTypeInformation = getJavaObject().getClass().equals(expectedJavaType); + + String declaredType = alias; + if (declaredType==null) declaredType = config.getTypeRegistry().getTypeName(getJavaObject()); + + // then for value + // if all keys are primitives then write as map { keyxxx: valuexxx } + // else write as list of singleton maps as above or kv pairs - [ { key: keyxxx , value: valuexxx } ] + // (in both cases using generics if available) + boolean allKeysString = true; + for (Object k: jo.keySet()) { + if (!(k instanceof String)) { allKeysString = false; break; } + } + + Object result; + boolean isEmpty; + if (allKeysString) { + if (isPureJson && genericValueSubType==null) { + genericValueSubType = YomlUtils.TYPE_JSON; + } + + MutableMap out = MutableMap.of(); + for (Map.Entry me: jo.entrySet()) { + Object v = converter.write( ((YomlContextForWrite)context).subpath("/"+me.getKey(), me.getValue(), genericValueSubType) ); + out.put(me.getKey(), v); + } + isEmpty = out.isEmpty(); + result = out; + + } else { + int i=0; + MutableList out = MutableList.of(); + for (Map.Entry me: jo.entrySet()) { + Object v = converter.write( ((YomlContextForWrite)context).subpath("/@value["+i+"]", me.getValue(), genericValueSubType) ); + + if (me.getKey() instanceof String) { + out.add(MutableMap.of(me.getKey(), v)); + } else { + Object k = converter.write( ((YomlContextForWrite)context).subpath("/@key["+i+"]", me.getKey(), genericKeySubType) ); + out.add(MutableMap.of("key", k, "value", v)); + } + i++; + + } + isEmpty = out.isEmpty(); + result = out; + } + + if (!isEmpty && ((!allKeysString && genericKeySubType!=null) || genericValueSubType!=null)) { + // if relying on generics we must include the types + if (writeWithoutTypeInformation) { + boolean mustWrap = false; + mustWrap |= (!allKeysString && genericKeySubType!=null && !Objects.equal(expectedGenericKeySubType, genericKeySubType)); + mustWrap |= (genericValueSubType!=null && !Objects.equal(expectedGenericValueSubType, genericValueSubType)); + if (mustWrap) { + writeWithoutTypeInformation = false; + } + } + declaredType = declaredType + "<" + (allKeysString ? "string" : genericKeySubType!=null ? genericKeySubType : "object") + "," + + (genericValueSubType!=null ? genericValueSubType : "object") + ">"; + } + + if (!writeWithoutTypeInformation) { + result = MutableMap.of("type", declaredType, "value", result); + } + + + storeWriteObjectAndAdvance(result); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypePrimitive.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypePrimitive.java new file mode 100644 index 0000000000..faa0e906a3 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypePrimitive.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.annotations.YomlAsPrimitive; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +public class InstantiateTypePrimitive extends YomlSerializerComposition { + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends InstantiateTypeWorkerAbstract { + + @Override + public Class getExpectedTypeJava() { + Class result = super.getExpectedTypeJava(); + if (result!=null) return result; + return getSpecialKnownTypeName(context.getExpectedType()); + } + + public void read() { + if (!canDoRead()) return; + + Class expectedJavaType; + Maybe value = Maybe.absent(); + + if (isJsonPrimitiveObject(getYamlObject())) { + // pure primitive - we must know the type and then we should simply be able to coerce + + expectedJavaType = getExpectedTypeJava(); + if (expectedJavaType==null && !isJsonMarkerTypeExpected()) return; + + // type should be coercible + value = tryCoerceAndNoteError(getYamlObject(), expectedJavaType); + if (value.isAbsent()) return; + + } else { + // not primitive; either should be coercible or should be of {type: ..., value: ...} format with type being the primitive + + expectedJavaType = getExpectedTypeJava(); + if (isDeferredValue(getYamlObject())) { + value = Maybe.of(getYamlObject()); + } + + if (value.isAbsent() && !isJsonComplexObject(getYamlObject()) && (expectedJavaType!=null || isJsonMarkerTypeExpected())) { + // try coercion as long as it's not a json map/list, and we've got an expectation + // maybe a bit odd to call that "primitive" but it is primitive in the sense it is pass-through unparsed + value = tryCoerceAndNoteError(getYamlObject(), expectedJavaType); + } + + if (value.isAbsent()) { + String typeName = readingTypeFromFieldOrExpected(); + if (typeName==null) return; + expectedJavaType = config.getTypeRegistry().getJavaTypeMaybe(typeName, context).orNull(); + if (expectedJavaType==null) expectedJavaType = getSpecialKnownTypeName(typeName); + // could restrict read coercion to basic types as follows, but no harm in trying to coerce if it's + // a value map, unless the target is a special json which will be handled by another serializer + if (isJsonComplexType(expectedJavaType) || isGeneric(typeName)) return; + + value = readingValueFromTypeValueMap(); + if (value.isAbsent()) return; + value = tryCoerceAndNoteError(value.get(), expectedJavaType); + if (value.isAbsent()) return; + removeTypeAndValueKeys(); + } + } + + storeReadObjectAndAdvance(value.get(), false); + } + + public void write() { + if (!canDoWrite()) return; + + Object jIn = getJavaObject(); + Object jOut = null; + + if (jIn==null) return; + + if (!YomlUtils.JsonMarker.isPureJson(jIn)) { + // not json, but can we coerce to json? + if (jIn.getClass().getAnnotation(YomlAsPrimitive.class)!=null) { + Object jo; + if (jOut==null) { + jo = config.getCoercer().tryCoerce(jIn, String.class).orNull(); + if (isReverseCoercible(jo, jIn)) jOut = jo; + } + if (jOut==null) { + jo = jIn.toString(); + if (isReverseCoercible(jo, jIn)) jOut = jo; + } + // could convert to other primitives eg int + // but no good use case so far + } + if (jOut!=null) { + // check whether we'll be able to read it back without additional type information + Maybe typeNeededCheck = config.getCoercer().tryCoerce(jOut, getExpectedTypeJava()); + if (typeNeededCheck.isPresent() && jIn.equals(typeNeededCheck.get())) { + // expected type is good enough to coerce, so write without type info + storeWriteObjectAndAdvance(jOut); + return; + + } else { + // fall through to below and write as type/value map + } + } + } + + if (jOut==null) jOut = jIn; + if (!YomlUtils.JsonMarker.isPureJson(jOut)) { + // it input is not pure json at this point, we don't apply + return; + } + + if (isJsonPrimitiveType(getExpectedTypeJava()) || isJsonMarkerTypeExpected()) { + // store it as pure primitive + storeWriteObjectAndAdvance(jOut); + return; + } + + // not expecting a primitive/json; bail out if it's not a primitive (map/list might decide to write `json` as the type) + if (!isJsonPrimitiveObject(jOut)) return; + + String typeName = config.getTypeRegistry().getTypeName(jIn); + if (addSerializersForDiscoveredRealType(typeName, true)) { + // if new serializers, bail out and we'll re-run + context.phaseRestart(); + return; + } + + MutableMap map = writingMapWithTypeAndLiteralValue(typeName, jOut); + context.phaseInsert(YomlContext.StandardPhases.MANIPULATING); + storeWriteObjectAndAdvance(map); + } + + private boolean isReverseCoercible(Object input, Object target) { + Maybe coerced = config.getCoercer().tryCoerce(input, target.getClass()); + if (coerced.isAbsent()) return false; + return (target.equals(coerced.get())); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeWorkerAbstract.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeWorkerAbstract.java new file mode 100644 index 0000000000..5eed07d9bd --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/InstantiateTypeWorkerAbstract.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.internal.SerializersOnBlackboard; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.serializers.YomlSerializerComposition.YomlSerializerWorker; + +public abstract class InstantiateTypeWorkerAbstract extends YomlSerializerWorker { + + /** puts the given label on the blackboard, in a namespace qualified to this class type, with the given value. + * returns whether the value is new on the blackboard, ie false if the value is already there. + */ + protected boolean putLabelOnBlackboard(String label, Object value) { + return !Objects.equals(value, blackboard.put(getClass().getName()+":"+label, value)); + } + + protected boolean canDoRead() { + if (!context.isPhase(YomlContext.StandardPhases.HANDLING_TYPE)) return false; + if (hasJavaObject()) return false; + return true; + } + + protected boolean canDoWrite() { + if (!context.isPhase(YomlContext.StandardPhases.HANDLING_TYPE)) return false; + if (hasYamlObject()) return false; + if (!hasJavaObject()) return false; + if (JavaFieldsOnBlackboard.isPresent(blackboard)) return false; + return true; + } + + /** invoked on read and write to apply the appropriate serializers one the real type is known, + * e.g. by looking up in registry. name of type will not be null but if it equals the java type + * that may mean that annotation-scanning is appropriate. */ + protected boolean addSerializersForDiscoveredRealType(@Nullable String type, boolean isRootType) { + if (type!=null) { + // (if null, we were writing what was expected, and we'll have added from expected type serializers) + if (!type.equals(context.getExpectedType())) { + if (putLabelOnBlackboard("discovered-type="+type, true)) { + SerializersOnBlackboard.get(blackboard).addInstantiatedTypeSerializers(config.getTypeRegistry().getSerializersForType(type, + isRootType ? context : context.subpath("...", null, null) )); + return true; + } + } + } + return false; + } + + protected void storeReadObjectAndAdvance(Object result, boolean addPhases) { + if (addPhases) { + context.phaseInsert(YomlContext.StandardPhases.MANIPULATING, YomlContext.StandardPhases.HANDLING_FIELDS); + } + context.setJavaObject(result); + context.phaseAdvance(); + } + + protected Maybe tryCoerceAndNoteError(Object value, Class expectedJavaType) { + if (expectedJavaType==null) return Maybe.of(value); + Maybe coerced = config.getCoercer().tryCoerce(value, expectedJavaType); + if (coerced.isAbsent()) { + // type present but not coercible - error + ReadingTypeOnBlackboard.get(blackboard).addNote("Cannot interpret or coerce '"+value+"' as "+ + config.getTypeRegistry().getTypeNameOfClass(expectedJavaType)); + } + return coerced; + } + + protected void storeWriteObjectAndAdvance(Object jo) { + context.setYamlObject(jo); + context.phaseAdvance(); + } + + protected String readingTypeFromFieldOrExpected() { + String type = null; + if (isYamlMap()) { + getYamlKeysOnBlackboardInitializedFromYamlMap(); + type = peekFromYamlKeysOnBlackboardRemaining("type", String.class).orNull(); + } + if (type==null) type = context.getExpectedType(); + return type; + } + protected Maybe readingValueFromTypeValueMap() { + return readingValueFromTypeValueMap(null); + } + protected Maybe readingValueFromTypeValueMap(Class requiredType) { + if (!isYamlMap()) return Maybe.absent(); + if (YamlKeysOnBlackboard.peek(blackboard).size()>2) return Maybe.absent(); + if (!YamlKeysOnBlackboard.peek(blackboard).hasKeysLeft("type", "value")) { + return Maybe.absent(); + } + return peekFromYamlKeysOnBlackboardRemaining("value", requiredType); + } + protected void removeTypeAndValueKeys() { + removeFromYamlKeysOnBlackboardRemaining("type", "value"); + } + + /** null type-name means we are writing the expected type */ + protected MutableMap writingMapWithType(@Nullable String typeName) { + JavaFieldsOnBlackboard.create(blackboard).fieldsToWriteFromJava = MutableList.of(); + MutableMap map = MutableMap.of(); + + if (typeName!=null) { + map.put("type", typeName); + } + return map; + } + protected MutableMap writingMapWithTypeAndLiteralValue(String typeName, Object value) { + MutableMap map = writingMapWithType(typeName); + if (value!=null) { + map.put("value", value); + } + return map; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/JavaFieldsOnBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/JavaFieldsOnBlackboard.java new file mode 100644 index 0000000000..766ecccd96 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/JavaFieldsOnBlackboard.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.YomlRequirement; +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +/** Indicates that something has handled the type + * (on read, creating the java object, and on write, setting the `type` field in the yaml object) + * and made a determination of what fields need to be handled */ +public class JavaFieldsOnBlackboard implements YomlRequirement { + + private static String KEY = JavaFieldsOnBlackboard.class.getName(); + + public static boolean isPresent(Map blackboard) { + return isPresent(blackboard, null); + } + public static JavaFieldsOnBlackboard peek(Map blackboard) { + return peek(blackboard, null); + } + public static JavaFieldsOnBlackboard getOrCreate(Map blackboard) { + return getOrCreate(blackboard, null); + } + public static JavaFieldsOnBlackboard create(Map blackboard) { + return create(blackboard, null); + } + + private static String key(String label) { + return KEY + (Strings.isNonBlank(label) ? ":"+label : ""); + } + public static boolean isPresent(Map blackboard, String label) { + return blackboard.containsKey(key(label)); + } + public static JavaFieldsOnBlackboard peek(Map blackboard, String label) { + return (JavaFieldsOnBlackboard) blackboard.get(key(label)); + } + public static JavaFieldsOnBlackboard getOrCreate(Map blackboard, String label) { + if (!isPresent(blackboard)) { blackboard.put(key(label), new JavaFieldsOnBlackboard()); } + return peek(blackboard, label); + } + public static JavaFieldsOnBlackboard create(Map blackboard, String label) { + if (isPresent(blackboard)) { throw new IllegalStateException("Already present"); } + blackboard.put(key(label), new JavaFieldsOnBlackboard()); + return peek(blackboard, label); + } + + List fieldsToWriteFromJava; + + String typeNameFromReadToConstructJavaLater; + Class typeFromReadToConstructJavaLater; + Map fieldsFromReadToConstructJava; + Map configToWriteFromJava; + + + @Override + public void checkCompletion(YomlContext context) { + if (fieldsToWriteFromJava!=null && !fieldsToWriteFromJava.isEmpty()) { + throw new YomlException("Incomplete write of Java object data: "+fieldsToWriteFromJava, context); + } + if (fieldsFromReadToConstructJava!=null && !fieldsFromReadToConstructJava.isEmpty()) { + throw new YomlException("Incomplete use of constructor fields creating Java object: "+fieldsFromReadToConstructJava, context); + } + if (configToWriteFromJava!=null && !configToWriteFromJava.isEmpty()) { + throw new YomlException("Incomplete write of config keys: "+configToWriteFromJava, context); + } + } + + @Override + public String toString() { + return super.toString()+"("+fieldsToWriteFromJava+"; "+typeNameFromReadToConstructJavaLater+": "+fieldsFromReadToConstructJava+")"; + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ReadingTypeOnBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ReadingTypeOnBlackboard.java new file mode 100644 index 0000000000..a7901c9fc2 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/ReadingTypeOnBlackboard.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.YomlRequirement; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; + +public class ReadingTypeOnBlackboard implements YomlRequirement { + + Set errorNotes = MutableSet.of(); + + public static final String KEY = ReadingTypeOnBlackboard.class.getCanonicalName(); + + public static ReadingTypeOnBlackboard get(Map blackboard) { + Object v = blackboard.get(KEY); + if (v==null) { + v = new ReadingTypeOnBlackboard(); + blackboard.put(KEY, v); + } + return (ReadingTypeOnBlackboard) v; + } + + @Override + public void checkCompletion(YomlContext context) { + if (context instanceof YomlContextForRead && context.getJavaObject()!=null) return; + if (context instanceof YomlContextForWrite && context.getYamlObject()!=null) return; + if (errorNotes.isEmpty()) throw new YomlException("No means to identify type to instantiate", context); + List messages = MutableList.of(); + List throwables = MutableList.of(); + for (Object errorNote: errorNotes) { + if (errorNote instanceof Throwable) { + messages.add(Exceptions.collapseText((Throwable)errorNote)); + throwables.add((Throwable)errorNote); + } else { + messages.add(Strings.toString(errorNote)); + } + } + throw new YomlException(Strings.join(messages, "; "), context, + throwables.isEmpty() ? null : + throwables.size()==1 ? throwables.iterator().next() : + Exceptions.create(throwables)); + } + + public void addNote(String message) { + errorNotes.add(message); + } + public void addNote(Throwable message) { + errorNotes.add(message); + } + + @Override + public String toString() { + return super.toString()+"["+errorNotes.size()+" notes]"; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/RenameKeySerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/RenameKeySerializer.java new file mode 100644 index 0000000000..a9237984fa --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/RenameKeySerializer.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.util.javalang.JavaClassNames; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultValue; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@YomlAllFieldsTopLevel +@Alias("rename-key") +public class RenameKeySerializer extends YomlSerializerComposition { + + private static final Logger log = LoggerFactory.getLogger(RenameKeySerializer.class); + + RenameKeySerializer() { } + + public RenameKeySerializer(YomlRenameKey ann) { + this(ann.oldKeyName(), ann.newKeyName(), YomlUtils.extractDefaultMap(ann.defaults())); + } + + public RenameKeySerializer(String oldKeyName, String newKeyName, Map defaults) { + super(); + this.oldKeyName = oldKeyName; + this.newKeyName = newKeyName; + this.defaults = defaults; + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + @Alias("from") + String oldKeyName; + @Alias("to") + String newKeyName; + Map defaults; + + public class Worker extends YomlSerializerWorker { + public void read() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + if (!isYamlMap()) return; + // this can run after type instantiation for the purpose of setting fields + + YamlKeysOnBlackboard ym = getYamlKeysOnBlackboardInitializedFromYamlMap(); + + if (!ym.hasKeysLeft(oldKeyName)) return; + if (ym.hadKeysEver(newKeyName)) return; + + ym.putNewKey(newKeyName, ym.removeKey(oldKeyName).get()); + ym.addDefaults(defaults); + + if (log.isTraceEnabled()) { + log.trace(this+" read, keys left now: "+ym); + } + + context.phaseRestart(); + } + + public void write() { + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return; + if (!isYamlMap()) return; + + // reverse order + if (!getOutputYamlMap().containsKey(newKeyName)) return; + if (getOutputYamlMap().containsKey(oldKeyName)) return; + + getOutputYamlMap().put(oldKeyName, getOutputYamlMap().remove(newKeyName)); + YomlUtils.removeDefaults(defaults, getOutputYamlMap()); + + if (log.isTraceEnabled()) { + log.trace(this+" write, output now: "+getOutputYamlMap()); + } + + context.phaseRestart(); + } + } + + @Override + public String toString() { + return JavaClassNames.simpleClassName(getClass())+"["+oldKeyName+"->"+newKeyName+"]"; + } + + @YomlAllFieldsTopLevel + @Alias("rename-default-key") + public static class RenameDefaultKey extends RenameKeySerializer { + public RenameDefaultKey(YomlRenameDefaultKey ann) { + this(ann.value(), YomlUtils.extractDefaultMap(ann.defaults())); + } + + public RenameDefaultKey(String newKeyName, Map defaults) { + super(".key", newKeyName, defaults); + } + } + + @YomlAllFieldsTopLevel + @Alias("rename-default-value") + public static class RenameDefaultValue extends RenameKeySerializer { + public RenameDefaultValue(YomlRenameDefaultValue ann) { + this(ann.value(), YomlUtils.extractDefaultMap(ann.defaults())); + } + + public RenameDefaultValue(String newKeyName, Map defaults) { + super(".value", newKeyName, defaults); + } + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelConfigKeySerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelConfigKeySerializer.java new file mode 100644 index 0000000000..e31e78134d --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelConfigKeySerializer.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.config.ConfigKey.HasConfigKey; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.YomlTypeFromOtherField; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TopLevelConfigKeySerializer extends TopLevelFieldSerializer { + + private static final Logger log = LoggerFactory.getLogger(TopLevelConfigKeySerializer.class); + + final String keyNameForConfigWhenSerialized; + + public TopLevelConfigKeySerializer(String keyNameForConfigWhenSerialized, ConfigKey configKey, Field optionalFieldForAnnotations) { + super(configKey.getName(), optionalFieldForAnnotations); + this.keyNameForConfigWhenSerialized = keyNameForConfigWhenSerialized; + this.configKey = configKey; + if (configKey.hasDefaultValue()) { + defaultValue = configKey.getDefaultValue(); + } + + // TODO yoml config key: + // - constraints + // - description + } + + /** The {@link ConfigKey#getName()} this serializer acts on */ + protected final ConfigKey configKey; + + @Override + protected String getKeyNameForMapOfGeneralValues() { + return keyNameForConfigWhenSerialized; + } + + @Override protected boolean includeFieldNameAsAlias() { return false; } + + public static Set> findConfigKeys(Class clazz) { + MutableMap> result = MutableMap.of(); + + for (Field f: YomlUtils.getAllNonTransientStaticFields(clazz).values()) { + try { + f.setAccessible(true); + Object ckO = f.get(null); + + ConfigKey ck = null; + if (ckO instanceof ConfigKey) ck = (ConfigKey)ckO; + else if (ckO instanceof HasConfigKey) ck = ((HasConfigKey)ckO).getConfigKey(); + + if (ck==null) continue; + if (result.containsKey(ck.getName())) continue; + + result.put(ck.getName(), ck); + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.warn("Unable to access static config key "+f+" (ignoring): "+e, e); + } + } + + return MutableSet.copyOf(result.values()); + } + + /** only useful in conjuction with {@link InstantiateTypeFromRegistryUsingConfigMap} static serializer factory methods */ + public static Set findConfigKeySerializers(String keyNameForConfigWhenSerialized, Class clazz) { + MutableMap resultKeys = MutableMap.of(); + Set resultOthers = MutableSet.of(); + + for (Field f: YomlUtils.getAllNonTransientStaticFields(clazz).values()) { + try { + f.setAccessible(true); + Object ckO = f.get(null); + + ConfigKey ck = null; + if (ckO instanceof ConfigKey) ck = (ConfigKey)ckO; + else if (ckO instanceof HasConfigKey) ck = ((HasConfigKey)ckO).getConfigKey(); + + if (ck==null) continue; + if (resultKeys.containsKey(ck.getName())) continue; + + resultKeys.put(ck.getName(), new TopLevelConfigKeySerializer(keyNameForConfigWhenSerialized, ck, f)); + + YomlTypeFromOtherField typeFromOther = f.getAnnotation(YomlTypeFromOtherField.class); + if (typeFromOther!=null) { + resultOthers.add(new TypeFromOtherFieldSerializer(ck.getName(), typeFromOther)); + } + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + log.warn("Unable to access static config key "+f+" (ignoring): "+e, e); + } + + } + + return MutableSet.copyOf(resultKeys.values()).putAll(resultOthers); + } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends TopLevelFieldSerializer.Worker { + protected boolean canDoRead() { + return !hasJavaObject() && context.willDoPhase(InstantiateTypeFromRegistryUsingConfigMap.PHASE_INSTANTIATE_TYPE_DEFERRED); + } + + @Override + protected void prepareTopLevelFields() { + super.prepareTopLevelFields(); + getTopLevelFieldsBlackboard().recordConfigKey(fieldName, configKey); + } + + @Override + protected boolean setDefaultValue(Map fields, int keysMatched) { + // no need to set the default for config keys + return false; + } + } + + @Override + protected String toStringPrefix() { return "top-level-config"; } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldSerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldSerializer.java new file mode 100644 index 0000000000..b30f4cdbdf --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldSerializer.java @@ -0,0 +1,306 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContext.StandardPhases; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Objects; + +/* On read, after InstantiateType populates the `fields` key in YamlKeysOnBlackboard, + * look for any field(s) matching known aliases and rename them there as the fieldName, + * so FieldsInMapUnderFields will then set it in the java object correctly. + *

+ * On write, after FieldsInMapUnderFields sets the `fields` map, + * look for the field name, and rewrite under the preferred alias at the root. */ +@YomlAllFieldsTopLevel +@Alias("top-level-field") +public class TopLevelFieldSerializer extends YomlSerializerComposition { + + private static final Logger log = LoggerFactory.getLogger(TopLevelFieldSerializer.class); + + public TopLevelFieldSerializer() {} + public TopLevelFieldSerializer(Field f) { + this(f.getName(), f); + } + /** preferred constructor for dealing with shadowed fields using superclass.field naming convention */ + public TopLevelFieldSerializer(String name, Field f) { + fieldName = keyName = name; + // if field is called type insert _ to prevent confusion + if (keyName.matches("_*type")) { + keyName = "_"+keyName; + } + + Alias alias = f.getAnnotation(Alias.class); + if (alias!=null) { + aliases = MutableList.of(); + if (Strings.isNonBlank(alias.preferred())) { + keyName = alias.preferred(); + aliases.add(alias.preferred()); + } + if (includeFieldNameAsAlias()) { + aliases.add(f.getName()); + } + aliases.addAll(Arrays.asList(alias.value())); + } + + // if there are other things on ytf +// YomlFieldAtTopLevel ytf = f.getAnnotation(YomlFieldAtTopLevel.class); + } + + protected boolean includeFieldNameAsAlias() { return true; } + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + /** field in java class to read/write */ + protected String fieldName; + + // not used at present, but would simplify expressing default values + // TODO we could also conceivably infer the expected field type better +// protected String fieldType; + + /** key to write at root in yaml */ + protected String keyName; + /** convenience if supplying a single item in {@link #aliases} */ + protected String alias; + /** aliases to recognise at root in yaml when reading, in addition to {@link #keyName} and normally {@link #fieldName} */ + protected List aliases; + + /** by default when multiple top-level-field serializers are supplied for the same {@link #fieldName}, all aliases are accepted; + * set this false to restrict to those in the first such serializer */ + protected Boolean aliasesInherited; + /** by default aliases are taken case-insensitive, with mangling supported, + * and including the {@link #fieldName} as an alias; + * set false to disallow all these, recognising only the explicitly noted + * {@link #keyName} and {@link #aliases} as keys (but still defaulting to {@link #fieldName} if {@link #keyName} is absent) */ + protected Boolean aliasesStrict; + + public static enum FieldConstraint { REQUIRED } + /** by default fields can be left null; set {@link FieldConstraint#REQUIRED} to require a value to be supplied (or a default set); + * other constraints may be introduded, and API may change, but keyword `required` will be coercible to this */ + protected FieldConstraint constraint; + + /** a default value to use when reading (and to use to determine whether to omit the field when writing) */ + // TODO would be nice to support maybe here, not hard here, but it makes it hard to set from yaml + // also keyword `default` as alias + protected Object defaultValue; + + protected String getKeyNameForMapOfGeneralValues() { + return FieldsInMapUnderFields.KEY_NAME_FOR_MAP_OF_FIELD_VALUES; + } + + public class Worker extends YomlSerializerWorker { + + final static String PREPARING_TOP_LEVEL_FIELDS = "preparing-top-level-fields"; + + protected String getPreferredKeyName() { + String result = getTopLevelFieldsBlackboard().getKeyName(fieldName); + if (result!=null) return result; + return fieldName; + } + + protected TopLevelFieldsBlackboard getTopLevelFieldsBlackboard() { + return TopLevelFieldsBlackboard.get(blackboard, getKeyNameForMapOfGeneralValues()); + } + + protected Iterable getKeyNameAndAliases() { + MutableSet keyNameAndAliases = MutableSet.of(); + keyNameAndAliases.addIfNotNull(getPreferredKeyName()); + if (!getTopLevelFieldsBlackboard().isAliasesStrict(fieldName)) { + keyNameAndAliases.addIfNotNull(fieldName); + } + keyNameAndAliases.addAll(getTopLevelFieldsBlackboard().getAliases(fieldName)); + return keyNameAndAliases; + } + + protected boolean readyForMainEvent() { + if (!context.seenPhase(YomlContext.StandardPhases.HANDLING_TYPE)) return false; + if (context.willDoPhase(YomlContext.StandardPhases.HANDLING_TYPE)) return false; + if (!context.seenPhase(PREPARING_TOP_LEVEL_FIELDS)) { + if (context.isPhase(YomlContext.StandardPhases.MANIPULATING)) { + // interrupt the manipulating phase to do a preparing phase + context.phaseInsert(PREPARING_TOP_LEVEL_FIELDS, StandardPhases.MANIPULATING); + context.phaseAdvance(); + return false; + } + } + if (context.isPhase(PREPARING_TOP_LEVEL_FIELDS)) { + prepareTopLevelFields(); + return false; + } + if (getTopLevelFieldsBlackboard().isFieldDone(fieldName)) return false; + if (!context.isPhase(YomlContext.StandardPhases.MANIPULATING)) return false; + return true; + } + + protected void prepareTopLevelFields() { + // do the pre-main pass to determine what is required for top-level fields and what the default is + getTopLevelFieldsBlackboard().setKeyNameIfUnset(fieldName, keyName); + getTopLevelFieldsBlackboard().addAliasIfNotDisinherited(fieldName, alias); + getTopLevelFieldsBlackboard().addAliasesIfNotDisinherited(fieldName, aliases); + getTopLevelFieldsBlackboard().setAliasesInheritedIfUnset(fieldName, aliasesInherited); + getTopLevelFieldsBlackboard().setAliasesStrictIfUnset(fieldName, aliasesStrict); + getTopLevelFieldsBlackboard().setConstraintIfUnset(fieldName, constraint); + if (getTopLevelFieldsBlackboard().getDefault(fieldName).isAbsent() && defaultValue!=null) { + getTopLevelFieldsBlackboard().setUseDefaultFrom(fieldName, TopLevelFieldSerializer.this, defaultValue); + } + // TODO combine aliases, other items + } + + protected boolean canDoRead() { return hasJavaObject(); } + + public void read() { + if (!readyForMainEvent()) return; + if (!canDoRead()) return; + if (!isYamlMap()) return; + + boolean fieldsCreated = false; + + @SuppressWarnings("unchecked") + Map fields = peekFromYamlKeysOnBlackboardRemaining(getKeyNameForMapOfGeneralValues(), Map.class).orNull(); + if (fields==null) { + // create the fields if needed; FieldsInFieldsMap will remove (even if empty) + fieldsCreated = true; + fields = MutableMap.of(); + // fine (needed even) to write this even with empty + getYamlKeysOnBlackboardInitializedFromYamlMap().putNewKey(getKeyNameForMapOfGeneralValues(), fields); + } + + int keysMatched = 0; + for (String aliasO: getKeyNameAndAliases()) { + Set aliasMangles = getTopLevelFieldsBlackboard().isAliasesStrict(fieldName) ? + Collections.singleton(aliasO) : findAllYamlKeysOnBlackboardRemainingMangleMatching(aliasO); + for (String alias: aliasMangles) { + Maybe value = peekFromYamlKeysOnBlackboardRemaining(alias, Object.class); + if (value.isAbsent()) continue; + if (log.isTraceEnabled()) { + log.trace(TopLevelFieldSerializer.this+": found "+alias+" for "+fieldName); + } + boolean fieldAlreadyKnown = fields.containsKey(fieldName); + if (value.isPresent() && fieldAlreadyKnown) { + // already present + if (!Objects.equal(value.get(), fields.get(fieldName))) { + throw new IllegalStateException("Cannot set '"+fieldName+"' to '"+value.get()+"' supplied in '"+alias+"' because this conflicts with '"+fields.get(fieldName)+"' already set"); + } + continue; + } + // value present, field not yet handled + removeFromYamlKeysOnBlackboardRemaining(alias); + fields.put(fieldName, value.get()); + keysMatched++; + } + } + + if (keysMatched==0) { + if (setDefaultValue(fields, keysMatched)) keysMatched++; + } + + if (fieldsCreated || keysMatched>0) { + // repeat this manipulating phase if we set any keys, so that remapping can apply + getTopLevelFieldsBlackboard().setFieldDone(fieldName); + context.phaseInsert(StandardPhases.MANIPULATING); + } + } + + protected boolean setDefaultValue(Map fields, int keysMatched) { + Maybe value = getTopLevelFieldsBlackboard().getDefault(fieldName); + if (!value.isPresentAndNonNull()) return false; + fields.put(fieldName, value.get()); + return true; + } + + public void write() { + if (!readyForMainEvent()) return; + if (!isYamlMap()) return; + + @SuppressWarnings("unchecked") + Map fields = getFromOutputYamlMap(getKeyNameForMapOfGeneralValues(), Map.class).orNull(); + /* + * if fields is null either we are too early (not yet set by instantiate-type / FieldsInMapUnderFields) + * or too late (already read in to java), so we bail -- this yaml key cannot be handled at this time + */ + if (fields==null) return; + + Maybe dv = getTopLevelFieldsBlackboard().getDefault(fieldName); + Maybe valueToSet; + + if (!fields.containsKey(fieldName)) { + // field not present, so omit (if field is not required and no default, or if default value is present and null) + // else write an explicit null + if ((dv.isPresent() && dv.isNull()) || (getTopLevelFieldsBlackboard().getConstraint(fieldName).orNull()!=FieldConstraint.REQUIRED && dv.isAbsent())) { + // if default is null, or if not required and no default, we can suppress + getTopLevelFieldsBlackboard().setFieldDone(fieldName); + return; + } + // default is non-null or field is required, so write the explicit null + valueToSet = Maybe.ofAllowingNull(null); + } else { + // field present + valueToSet = Maybe.of(fields.remove(fieldName)); + if (dv.isPresent() && Objects.equal(dv.get(), valueToSet.get())) { + // suppress if it equals the default + getTopLevelFieldsBlackboard().setFieldDone(fieldName); + valueToSet = Maybe.absent(); + } + } + + if (valueToSet.isPresent()) { + getTopLevelFieldsBlackboard().setFieldDone(fieldName); + Object oldValue = getOutputYamlMap().put(getPreferredKeyName(), valueToSet.get()); + if (oldValue!=null && !oldValue.equals(valueToSet.get())) { + throw new YomlException("Conflicting values for `"+getPreferredKeyName()+"`: "+oldValue+" / "+valueToSet.get(), context); + } + // and move the `fields` object to the end + getOutputYamlMap().remove(getKeyNameForMapOfGeneralValues()); + if (!fields.isEmpty()) + getOutputYamlMap().put(getKeyNameForMapOfGeneralValues(), fields); + // rerun this phase again, as we've changed it + context.phaseInsert(StandardPhases.MANIPULATING); + } else if (fields.isEmpty()) { + getOutputYamlMap().remove(getKeyNameForMapOfGeneralValues()); + } + } + } + + protected String toStringPrefix() { return "top-level-field"; } + + @Override + public String toString() { + return toStringPrefix()+"["+fieldName+"->"+keyName+":"+alias+"/"+aliases+"]"; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldsBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldsBlackboard.java new file mode 100644 index 0000000000..55b5a25020 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TopLevelFieldsBlackboard.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.YomlRequirement; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.serializers.TopLevelFieldSerializer.FieldConstraint; + +public class TopLevelFieldsBlackboard implements YomlRequirement { + + public static final String KEY = TopLevelFieldsBlackboard.class.getCanonicalName(); + + public static TopLevelFieldsBlackboard get(Map blackboard, String mode) { + Object v = blackboard.get(KEY+":"+mode); + if (v==null) { + v = new TopLevelFieldsBlackboard(); + blackboard.put(KEY+":"+mode, v); + } + return (TopLevelFieldsBlackboard) v; + } + + private final Map keyNames = MutableMap.of(); + private final Map aliasesInheriteds = MutableMap.of(); + private final Map aliasesStricts = MutableMap.of(); + private final Map> aliases = MutableMap.of(); + private final Set fieldsDone = MutableSet.of(); + private final Map fieldsConstraints = MutableMap.of(); + private final Map defaultValueForFieldComesFromSerializer = MutableMap.of(); + private final Map defaultValueOfField = MutableMap.of(); + private final Map> keyForFieldsAndAliases = MutableMap.of(); + + public String getKeyName(String fieldName) { + return Maybe.ofDisallowingNull(keyNames.get(fieldName)).orNull(); + } + public void setKeyNameIfUnset(String fieldName, String keyName) { + if (keyName==null) return; + if (keyNames.get(fieldName)!=null) return; + keyNames.put(fieldName, keyName); + } + public void setAliasesInheritedIfUnset(String fieldName, Boolean aliasesInherited) { + if (aliasesInherited==null) return; + if (aliasesInheriteds.get(fieldName)!=null) return; + aliasesInheriteds.put(fieldName, aliasesInherited); + } + public boolean isAliasesStrict(String fieldName) { + return Boolean.TRUE.equals(aliasesStricts.get(fieldName)); + } + public void setAliasesStrictIfUnset(String fieldName, Boolean aliasesStrict) { + if (aliasesStrict==null) return; + if (aliasesStricts.get(fieldName)!=null) return; + aliasesStricts.put(fieldName, aliasesStrict); + } + public void addAliasIfNotDisinherited(String fieldName, String alias) { + addAliasesIfNotDisinherited(fieldName, MutableList.of().appendIfNotNull(alias)); + } + public void addAliasesIfNotDisinherited(String fieldName, List aliases) { + if (Boolean.FALSE.equals(aliasesInheriteds.get(fieldName))) { + // no longer heritable + return; + } + Set aa = this.aliases.get(fieldName); + if (aa==null) { + aa = MutableSet.of(); + this.aliases.put(fieldName, aa); + } + if (aliases==null) return; + for (String alias: aliases) { if (Strings.isNonBlank(alias)) { aa.add(alias); } } + } + public Collection getAliases(String fieldName) { + Set aa = this.aliases.get(fieldName); + if (aa==null) return MutableSet.of(); + return aa; + } + + public Maybe getConstraint(String fieldName) { + return Maybe.ofDisallowingNull(fieldsConstraints.get(fieldName)); + } + public void setConstraintIfUnset(String fieldName, FieldConstraint constraint) { + if (constraint==null) return; + if (fieldsConstraints.get(fieldName)!=null) return; + fieldsConstraints.put(fieldName, constraint); + } + @Override + public void checkCompletion(YomlContext context) { + List incompleteRequiredFields = MutableList.of(); + for (Map.Entry fieldConstraint: fieldsConstraints.entrySet()) { + FieldConstraint v = fieldConstraint.getValue(); + if (v!=null && FieldConstraint.REQUIRED==v && !fieldsDone.contains(fieldConstraint.getKey())) { + incompleteRequiredFields.add(fieldConstraint.getKey()); + } + } + if (!incompleteRequiredFields.isEmpty()) { + throw new YomlException("Missing one or more explicitly required fields: "+Strings.join(incompleteRequiredFields, ", "), context); + } + } + + public boolean isFieldDone(String fieldName) { + return fieldsDone.contains(fieldName); + } + public void setFieldDone(String fieldName) { + fieldsDone.add(fieldName); + } + + public void setUseDefaultFrom(String fieldName, YomlSerializer topLevelField, Object defaultValue) { + defaultValueForFieldComesFromSerializer.put(fieldName, topLevelField); + defaultValueOfField.put(fieldName, defaultValue); + } + public boolean shouldUseDefaultFrom(String fieldName, YomlSerializer topLevelField) { + return topLevelField.equals(defaultValueForFieldComesFromSerializer.get(fieldName)); + } + public Maybe getDefault(String fieldName) { + if (!defaultValueOfField.containsKey(fieldName)) return Maybe.absent("no default"); + return Maybe.of(defaultValueOfField.get(fieldName)); + } + + /** optional, and must be called after aliases; records the config key for subsequent retrieval */ + public void recordConfigKey(String fieldName, ConfigKey key) { + setKeyForIndividualNameOrAliasIfUnset(fieldName, key); + for (String alias: getAliases(fieldName)) + setKeyForIndividualNameOrAliasIfUnset(alias, key); + } + protected void setKeyForIndividualNameOrAliasIfUnset(String fieldName, ConfigKey type) { + if (keyForFieldsAndAliases.get(fieldName)!=null) return; + keyForFieldsAndAliases.put(fieldName, type); + } + /** only if {@link #recordConfigKey(String, ConfigKey)} has been used */ + public ConfigKey getConfigKey(String fieldNameOrAlias) { + return keyForFieldsAndAliases.get(fieldNameOrAlias); + } + /** only if {@link #recordConfigKey(String, ConfigKey)} has been used */ + public Map> getConfigKeys() { + return MutableMap.copyOf(keyForFieldsAndAliases); + } + + @Override + public String toString() { + return super.toString()+"[keys "+keyNames.keySet()+",done "+fieldsDone+"]"; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldBlackboard.java new file mode 100644 index 0000000000..be63b0ccd8 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldBlackboard.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; + +import org.apache.brooklyn.util.collections.MutableMap; + +public class TypeFromOtherFieldBlackboard { + + public static final String KEY = TypeFromOtherFieldBlackboard.class.getCanonicalName(); + + public static TypeFromOtherFieldBlackboard get(Map blackboard) { + Object v = blackboard.get(KEY); + if (v==null) { + v = new TypeFromOtherFieldBlackboard(); + blackboard.put(KEY, v); + } + return (TypeFromOtherFieldBlackboard) v; + } + + private final Map typeConstraintField = MutableMap.of(); + private final Map typeConstraintFieldIsReal = MutableMap.of(); + + public void setTypeConstraint(String field, String typeField, boolean isReal) { + typeConstraintField.put(field, typeField); + typeConstraintFieldIsReal.put(field, isReal); + } + + public String getTypeConstraintField(String field) { + return typeConstraintField.get(field); + } + + public boolean isTypeConstraintFieldReal(String f) { + return typeConstraintFieldIsReal.get(f); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldSerializer.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldSerializer.java new file mode 100644 index 0000000000..0ce91914fd --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/TypeFromOtherFieldSerializer.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlTypeFromOtherField; +import org.apache.brooklyn.util.yoml.internal.YomlContext.StandardPhases; + +/** Populates a blackboard recording the fact, for use by {@link FieldsInMapUnderFields} */ +@YomlAllFieldsTopLevel +@Alias("type-from-other-field") +public class TypeFromOtherFieldSerializer extends YomlSerializerComposition { + + public TypeFromOtherFieldSerializer() {} + public TypeFromOtherFieldSerializer(String fieldName, YomlTypeFromOtherField otherFieldInfo) { + this(fieldName, otherFieldInfo.value(), otherFieldInfo.real()); + } + public TypeFromOtherFieldSerializer(String fieldNameToDecorate, String fieldNameContainingType, boolean isFieldReal) { + this.field = fieldNameToDecorate; + this.typeField = fieldNameContainingType; + this.typeFieldReal = isFieldReal; + } + + String field; + String typeField; + boolean typeFieldReal; + + protected YomlSerializerWorker newWorker() { + return new Worker(); + } + + public class Worker extends YomlSerializerWorker { + + public void go() { + // probably runs too often but optimize that later + TypeFromOtherFieldBlackboard.get(blackboard).setTypeConstraint(field, typeField, typeFieldReal); + } + + public void read() { + if (!context.isPhase(StandardPhases.MANIPULATING)) return; + if (!isYamlMap()) return; + go(); + } + + public void write() { + if (!context.isPhase(StandardPhases.HANDLING_TYPE)) return; + go(); + } + + } + + @Override + public String toString() { + return super.toString()+"["+field+"<-"+typeField+"]"; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YamlKeysOnBlackboard.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YamlKeysOnBlackboard.java new file mode 100644 index 0000000000..f4dab84504 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YamlKeysOnBlackboard.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.yoml.YomlException; +import org.apache.brooklyn.util.yoml.YomlRequirement; +import org.apache.brooklyn.util.yoml.internal.YomlContext; + +/** Keys from a YAML map that still need to be handled */ +public class YamlKeysOnBlackboard implements YomlRequirement { + + private static String KEY = YamlKeysOnBlackboard.class.getName(); + + public static boolean isPresent(Map blackboard) { + return blackboard.containsKey(KEY); + } + /** returns the {@link YamlKeysOnBlackboard} or null if not yet initialized */ + public static YamlKeysOnBlackboard peek(Map blackboard) { + return (YamlKeysOnBlackboard) blackboard.get(KEY); + } + /** deletes the {@link YamlKeysOnBlackboard} on the blackboard, so that it will be re-initialized from the YAML object */ + public static void delete(Map blackboard) { + blackboard.remove(KEY); + } + /** returns the {@link YamlKeysOnBlackboard}, creating from the given keys map if not yet present */ + public static YamlKeysOnBlackboard getOrCreate(Map blackboard, Map keys) { + if (!isPresent(blackboard)) { + YamlKeysOnBlackboard ykb = new YamlKeysOnBlackboard(); + blackboard.put(KEY, ykb); + ykb.yamlAllKeysEverToReadToJava = MutableMap.copyOf(keys); + ykb.yamlKeysRemainingToReadToJava = MutableMap.copyOf(keys); + } + return peek(blackboard); + } + public static YamlKeysOnBlackboard create(Map blackboard) { + if (isPresent(blackboard)) { throw new IllegalStateException("Already present"); } + blackboard.put(KEY, new YamlKeysOnBlackboard()); + return peek(blackboard); + } + + private Map yamlAllKeysEverToReadToJava; + private Map yamlKeysRemainingToReadToJava; + + @Override + public void checkCompletion(YomlContext context) { + if (!yamlKeysRemainingToReadToJava.isEmpty()) { + // TODO limit toString to depth 2 ? + throw new YomlException("Incomplete read of YAML keys: "+yamlKeysRemainingToReadToJava, context); + } + } + + @Override + public String toString() { + return super.toString()+"("+yamlKeysRemainingToReadToJava.size()+" ever; remaining="+yamlKeysRemainingToReadToJava+")"; + } + + /** clears keys remaining, normally indicating that work is done */ + public void clearRemaining() { + yamlKeysRemainingToReadToJava.clear(); + } + public boolean hasKeysLeft(String ...keys) { + for (String k: keys) { + if (!yamlKeysRemainingToReadToJava.containsKey(k)) return false; + } + return true; + } + public boolean hadKeysEver(String ...keys) { + for (String k: keys) { + if (!yamlAllKeysEverToReadToJava.containsKey(k)) return false; + } + return true; + } + public int size() { + return yamlKeysRemainingToReadToJava.size(); + } + public Maybe removeKey(String k) { + if (!yamlKeysRemainingToReadToJava.containsKey(k)) return Maybe.absent(); + return Maybe.of(yamlKeysRemainingToReadToJava.remove(k)); + } + public void putNewKey(String k, Object value) { + if (yamlKeysRemainingToReadToJava.put(k, value)!=null) throw new IllegalStateException("Already had value for "+k); + yamlAllKeysEverToReadToJava.put(k, value); + } + public int addDefaults(Map defaults) { + // like YomlUtils.addDefaults(...) but only adding if never seen + int count = 0; + if (defaults!=null) for (String key: defaults.keySet()) { + if (!yamlAllKeysEverToReadToJava.containsKey(key)) { + count++; + yamlAllKeysEverToReadToJava.put(key, defaults.get(key)); + yamlKeysRemainingToReadToJava.put(key, defaults.get(key)); + } + } + return count; + } + public Maybe peekKeyLeft(String k) { + if (!yamlKeysRemainingToReadToJava.containsKey(k)) return Maybe.absent(); + return Maybe.of(yamlKeysRemainingToReadToJava.get(k)); + } + public Set keysLeft() { + return yamlKeysRemainingToReadToJava.keySet(); + } + +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YomlSerializerComposition.java b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YomlSerializerComposition.java new file mode 100644 index 0000000000..f1a4ad83ab --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/serializers/YomlSerializerComposition.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.serializers; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; + +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlContextForWrite; +import org.apache.brooklyn.util.yoml.internal.YomlConverter; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.JsonMarker; + +import com.google.common.base.Supplier; + +public abstract class YomlSerializerComposition implements YomlSerializer { + + protected abstract YomlSerializerWorker newWorker(); + + public abstract static class YomlSerializerWorker { + + protected YomlConverter converter; + protected YomlContext context; + protected YomlContextForRead readContext; + protected YomlConfig config; + protected Map blackboard; + + + protected void warn(String message) { + ReadingTypeOnBlackboard.get(blackboard).addNote(message); + } + protected void warn(Throwable message) { + ReadingTypeOnBlackboard.get(blackboard).addNote(message); + } + + + protected boolean isJsonPrimitiveType(Class type) { + if (type==null) return false; + if (String.class.isAssignableFrom(type)) return true; + if (Boxing.isPrimitiveOrBoxedClass(type)) return true; + return false; + } + protected boolean isJsonTypeName(String typename) { + if (isJsonMarkerType(typename)) return true; + return getSpecialKnownTypeName(typename)!=null; + } + protected boolean isJsonMarkerTypeExpected() { + return isJsonMarkerType(context.getExpectedType()); + } + protected boolean isJsonMarkerType(String typeName) { + return YomlUtils.TYPE_JSON.equals(typeName); + } + protected Class getSpecialKnownTypeName(String typename) { + if (YomlUtils.TYPE_STRING.equals(typename)) return String.class; + if (YomlUtils.TYPE_LIST.equals(typename)) return List.class; + if (YomlUtils.TYPE_SET.equals(typename)) return Set.class; + if (YomlUtils.TYPE_MAP.equals(typename)) return Map.class; + return Boxing.boxedType( Boxing.getPrimitiveType(typename).orNull() ); + } + protected boolean isJsonComplexType(Class t) { + if (t==null) return false; + // or could be equals, used as response of the above + if (Map.class.isAssignableFrom(t)) return true; + if (Set.class.isAssignableFrom(t)) return true; + if (List.class.isAssignableFrom(t)) return true; + return false; + } + protected boolean isGeneric(String typename) { + if (typename==null) return false; + return typename.contains("<"); + } + + /** true iff the object is a string or java primitive type */ + protected boolean isJsonPrimitiveObject(Object o) { + if (o==null) return true; + if (o instanceof String) return true; + if (Boxing.isPrimitiveOrBoxedObject(o)) return true; + return false; + } + + /** true iff the object is a collection */ + protected boolean isJsonList(Object o) { + return (o instanceof Collection); + } + /** true iff the object is a map or collection (not recursing; for that see {@link #isJsonPureObject(Object)} */ + protected boolean isJsonComplexObject(Object o) { + return (o instanceof Map || isJsonList(o)); + } + + /** true iff the object is a primitive type or a map or collection of pure objects; + * see {@link JsonMarker#isPureJson(Object)} (which this simply proxies for convenience) */ + protected boolean isJsonPureObject(Object o) { + return YomlUtils.JsonMarker.isPureJson(o); + } + + protected boolean isDeferredValue(Object o) { + return o instanceof Supplier || o instanceof Future; + } + + private void initRead(YomlContextForRead context, YomlConverter converter) { + if (this.context!=null) throw new IllegalStateException("Already initialized, for "+context); + this.context = context; + this.readContext = context; + this.converter = converter; + this.config = converter.getConfig(); + this.blackboard = context.getBlackboard(); + } + + private void initWrite(YomlContextForWrite context, YomlConverter converter) { + if (this.context!=null) throw new IllegalStateException("Already initialized, for "+context); + this.context = context; + this.converter = converter; + this.config = converter.getConfig(); + this.blackboard = context.getBlackboard(); + } + + /** If there is an expected type -- other than "Object"! -- return the java instance. Otherwise null. */ + public Class getExpectedTypeJava() { + String et = context.getExpectedType(); + if (Strings.isBlank(et)) return null; + Class ett = config.getTypeRegistry().getJavaTypeMaybe(et, context).orNull(); + if (Object.class.equals(ett)) return null; + return ett; + } + + public boolean hasJavaObject() { return context.getJavaObject()!=null; } + public boolean hasYamlObject() { return context.getYamlObject()!=null; } + public Object getJavaObject() { return context.getJavaObject(); } + public Object getYamlObject() { return context.getYamlObject(); } + + /** Reports whether the YAML object is a map + * (or on read whether it has now been transformed to a map). */ + public boolean isYamlMap() { return getYamlObject() instanceof Map; } + + protected void assertReading() { assert context instanceof YomlContextForRead; } + protected void assertWriting() { assert context instanceof YomlContextForWrite; } + + @SuppressWarnings("unchecked") + public Map getOutputYamlMap() { + assertWriting(); + return (Map)context.getYamlObject(); + } + + @SuppressWarnings("unchecked") + public Map getRawInputYamlMap() { + assertReading(); + return (Map)context.getYamlObject(); + } + + /** Returns the value of the given key if it is present in the output map and is of the given type. + * If the YAML is not a map, or the key is not present, or the type is different, this returns an absent. + *

+ * Read serializers or anything interested in the state of the map should use + * {@link #peekFromYamlKeysOnBlackboardRemaining(String, Class)} and other methods here or + * {@link YamlKeysOnBlackboard} directly. */ + @SuppressWarnings("unchecked") + public Maybe getFromOutputYamlMap(String key, Class type) { + if (!isYamlMap()) return Maybe.absent("not a yaml map"); + if (!getOutputYamlMap().containsKey(key)) return Maybe.absent("key `"+key+"` not in yaml map"); + Object v = getOutputYamlMap().get(key); + if (v==null) return Maybe.ofAllowingNull(null); + if (!type.isInstance(v)) return Maybe.absent("value of key `"+key+"` is not a "+type); + return Maybe.of((T) v); + } + /** Writes directly to the yaml map which will be returned from a write. + * Read serializers should not use as per the comments on {@link #getFromOutputYamlMap(String, Class)}. */ + protected void setInOutputYamlMap(String key, Object value) { + ((Map)getOutputYamlMap()).put(key, value); + } + + /** creates a YKB instance. fails if the raw yaml input is not a map. */ + protected YamlKeysOnBlackboard getYamlKeysOnBlackboardInitializedFromYamlMap() { + return YamlKeysOnBlackboard.getOrCreate(blackboard, getRawInputYamlMap()); + } + + @SuppressWarnings("unchecked") + protected Maybe peekFromYamlKeysOnBlackboardRemaining(String key, Class expectedType) { + YamlKeysOnBlackboard ykb = YamlKeysOnBlackboard.peek(blackboard); + if (ykb==null) return Maybe.absent(); + Maybe v = ykb.peekKeyLeft(key); + if (v.isAbsent()) return Maybe.absent(); + if (expectedType!=null && !expectedType.isInstance(v.get())) return Maybe.absent(); + return Maybe.of((T)v.get()); + } + protected boolean hasYamlKeysOnBlackboardRemaining() { + YamlKeysOnBlackboard ykb = YamlKeysOnBlackboard.peek(blackboard); + return (ykb!=null && ykb.size()>0); + } + protected void removeFromYamlKeysOnBlackboardRemaining(String ...keys) { + YamlKeysOnBlackboard ykb = YamlKeysOnBlackboard.peek(blackboard); + for (String key: keys) { + ykb.removeKey(key); + } + } + /** looks for all keys in {@link YamlKeysOnBlackboard} which can be mangled/ignore-case + * to match the given key */ + protected Set findAllYamlKeysOnBlackboardRemainingMangleMatching(String targetKey) { + Set result = MutableSet.of(); + YamlKeysOnBlackboard ykb = YamlKeysOnBlackboard.peek(blackboard); + for (Object k: ykb.keysLeft()) { + if (k instanceof String && YomlUtils.mangleable(targetKey, (String)k)) { + result.add((String)k); + } + } + return result; + } + + public abstract void read(); + public abstract void write(); + } + + @Override + public void read(YomlContextForRead context, YomlConverter converter) { + YomlSerializerWorker worker; + try { + worker = newWorker(); + } catch (Exception e) { throw Exceptions.propagate(e); } + worker.initRead(context, converter); + worker.read(); + } + + @Override + public void write(YomlContextForWrite context, YomlConverter converter) { + YomlSerializerWorker worker; + try { + worker = newWorker(); + } catch (Exception e) { throw Exceptions.propagate(e); } + worker.initWrite(context, converter); + worker.write(); + } + + @Override + public String document(String type, YomlConverter converter) { + return null; + } +} diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/yoml/sketch.md b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/sketch.md new file mode 100644 index 0000000000..f497089172 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/yoml/sketch.md @@ -0,0 +1,654 @@ +% +% Licensed to the Apache Software Foundation (ASF) under one +% or more contributor license agreements. See the NOTICE file +% distributed with this work for additional information +% regarding copyright ownership. The ASF licenses this file +% to you under the Apache License, Version 2.0 (the +% "License"); you may not use this file except in compliance +% with the License. You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, +% software distributed under the License is distributed on an +% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +% KIND, either express or implied. See the License for the +% specific language governing permissions and limitations +% under the License. +% + +# YOML: The YAML Object Mapping Language + +## Motivation + +We want a JSON/YAML schema which allows us to do bi-directional serialization to Java with docgen. +That is: +* It is easy for a user to write the YAML which generates the objects they care about +* It is easy for a user to read the YAML generated from data objects +* The syntax of the YAML can be documented automatically from the schema (including code-point-completion) +* JSON can also be read or written (we restrict to the subset of YAML which is isomorphic to JSON) + +The focus on ease-of-reading and ease-of-writing differentiates this from other JSON/YAML +serialization processes. For instance we want to be able to support the following polymorphic +expressions: + +``` +shapes: +- type: square # standard, explicit type and fields, but hard to read + size: 12 + color: red +- square: # type implied by key + size: 12 + color: red +- square: 12 # value is taken as a default key in a map +- red_square # string on its own can be interpreted in many ways but often it's the type +# and optionally (deferred) +- red square: { size: 12 } # multi-word string could be parsed in many ways (a la css border) +``` + +Because in most contexts we have some sense of what we are expecting, we can get very efficient +readable representations. + +Of course you shouldn't use all of these to express the same type; but depending on the subject +matter some syntaxes may be more natural than others. Consider allowing writing: + +``` + effectors: # field in parent, expects list of types 'Effector' + say_hi: # map converts to list treating key as name of effector, expecting type 'Effector' as value + type: ssh # type alias 'ssh' when type 'Effector` is needed matches SshEffector type + parameters: # field in SshEffector, of type Parameter + - person # string given when Parameter expected means it's the parameter name + - name: hello_word # map of type, becomes a Parameter populating fields + description: how to say hello + default: hello + command: | # and now the command, which SshEffector expects + echo ${hello_word} ${person:-world} +``` + +The important thing here is not using all of them at the same time (as we did for shape), +but being *able* to support an author picking the subset that is right for a given situation, +in a way that they can be parsed, they can be generated, and the expected/supported syntax +can be documented automatically. + + +## Introductory Examples + + +### Defining types and instances + +You define a type by giving an `id` (how it is known) and an instance definition specifying the parent `type`. +These are kept in a type registry and can be used when defining other types or instances. +A "type definition" looks like: + +``` +- id: shape + definition: + type: java:org.acme.Shape # where `class Shape { String name; String color; }` +``` + +The `java:` prefix is an optional shorthand to allow a Java type to be accessed. +For now this assumes a no-arg constructor. + +You can then specify an instance to be created by giving an "instance definition", +referring to a defined `type` and optionally `fields`: + +``` +- type: shape + fields: # optionally + name: square + color: red +``` + +Type definitions can also refer to types already defined types and can give an instance definition: + +``` +- id: red-square + definition: + type: shape + fields: + # any fields here read/written by direct access by default, or fail if not matched + name: square + color: red +``` + +The heart of YOML is the extensible support for other syntaxes available, described below. +These lead to succinct and easy-to-use definitions, both for people to write and for people to read +even when machine-generated. The approach also supports documentation and code-point completion. + + +### Instance definitions + +You define an instance to be created by referencing a type in the registry, and optionally specifying fields: + + type: red-square + +Or + + type: shape + fields: + name: square + color: red + + +### Type definitions + +You define a new type in the registry by giving an `id` and the instance `definition`: + + id: red-square + definition: + type: shape + fields: + name: square + color: red + +Where you just want to define a Java class, a shorthand permits providing `type` instead of the `definition`: + + id: shape + type: java:org.acme.Shape + + +### Overwriting fields + +Fields can be overwritten, e.g. to get a pink square: + + type: red-square + fields: + # map of fields is merged with that of parent + color: pink + +You can do this in type definitions, so you could do this: + +``` +- id: pink-square + type: red-square + definition: + fields: + # map of fields is merged with that of parent + color: pink +``` + +Although this would be more sensible: + +``` +- id: square + definition: + type: shape + fields: + name: square +- id: pink-square + definition: + type: square + fields: + color: pink +``` + + +### Allowing fields at root + +Type definitions also support specifying additional "serializers", the workers which provide +alternate syntaxes. One common one is the `top-level-field` serializer, allowing fields at +the root of an instance definition. With this type defined: + +- id: ez-square + type: square + serialization: + - type: top-level-field + field-name: color + +You could skip the `fields` item altogether and write: + +``` +type: ez-square +color: pink +``` + +These are inherited, so we'd probably prefer to have these type definitions: + +``` +- id: shape + definition: + type: java:org.acme.Shape # where `class Shape { String name; String color; }` + serialization: + - type: top-level-field + field-name: name + - type: top-level-field + field-name: color +- id: square + definition: + type: shape + name: square +``` + +## Intermission: On serializers and implementation (can skip) + +Serialization takes a list of serializer types. These are applied in order, both for serialization +and deserialization, and re-run from the beginning if any are applied. + +`top-level-field` says to look at the root as well as in the 'fields' block. It has one required +parameter, field-name, and several optional ones, so a sample usage might look like: + +``` + - type: top-level-field + field-name: color + key-name: color # this is used in yaml + aliases: [ colour ] # things to accept in yaml as synonyms for key-name; `alias` also accepted + aliases-strict: false # if true, means only exact matches on key-name and aliases are accepted, otherwise a set of mangles are applied + aliases-inherited: true # if false, means only take aliases from the first top-level-field serializer for this field-name, otherwise any can be used + # TODO items below here are still WIP/planned + field-type: string # inferred from java field, but you can constrain further to yaml types + constraint: required # currently just supports 'required' (and 'null' not allowed) or blank for none (default), but reserved for future use + description: The color of the shape # text (markdown) + serialization: # optional additional serialization instructions for this field + - convert-from-primitive: # (defined below) + key: field-name +``` + + +## Further Behaviours + +### Name Mangling and Aliases + +We apply a default conversion for fields: +wherever pattern is lower-upper-lower (java) <-> lower-dash-lower-lower (yaml). +These are handled as a default set of aliases. + + fields: + # corresponds to field shapeColor + shape-color: red + + +### Primitive types + +All Java primitive types are known, with their boxed and unboxed names, +along with `string`. The key `value` can be used to set a value for these. +It's not normally necessary to do this because the parser can usually detect +these types and coercion will be applied wherever one is expected; +it's only needed if the value needs coercing and the target type isn't implicit. +For instance a red square with size 8 could be defined as: + +``` +- type: shape + color: + type: string + value: red + size: + type: int + value: 8 +``` + +Or of course the more concise: + +``` +- type: shape + color: red + size: 8 +``` + + +### Config/data keys + +Some java types define static ConfigKey fields and a `configure(key, value)` or `configure(ConfigBag)` +method. These are detected and applied as one of the default strategies (below). + + +### Accepting lists, including generics + +Where the java object is a list, this can correspond to YAML in many ways. +The simplest is where the YAML is a list, in which case each item is parsed, +including serializers for the generic type of the list if available. + +Because lists are common in object representation, and because the context +might have additional knowledge about how to intepret them, a list field +(or any context where a list is expected) can declare additional serializers. +This can result in much nicer YOML representations. + +Serializers available for this include: + +* `convert-singleton-map` which can specify how to convert a map to a list, + by taking each pair and treating it as a list of f() where + f maps the K and V into a new map, e.g. embedding K with a default key +* `default-map-values` which ensures a set of keys are present in every entry, + using default values wherever the key is absent +* `convert-singleton-maps-in-list` which gives special behaviour if a list consists + entirely of single-key-maps, useful where a user might want to supply a concise map + syntax but that map would have several keys the same +* `convert-from-primitive` which converts a primitive to a map + +If no special list serialization is supplied for when expecting a type of `list`, +the YAML must be a list and the serialization rules for `x` are then applied. If no +generic type is available for a list and no serialization is specified, an explicit +type is required on all entries. + +Serializations that apply to lists are applied to each entry, and if any apply the +serialization is then continued from the beginning (unless otherwise noted). + + +#### Complex list serializers (skip on first read!) + +At the heart of this YAML serialization is the idea of heavily overloading to permit the most +natural way of writing in different situations. We go a bit overboard in some of the `serialization` +examples to illustrate the different strategies and some of the subtleties. (Feel free to ignore +until and unless you need to know details of complex strategies.) + +As a complex example, to define serializations for shape, the basic syntax is as follows: + + serialization: + - type: top-level-field + field-name: color + alias: colour + description: "The color of the shape" + - type: top-level-field + field-name: name + - type: convert-from-primitive + key: color + +However we can also support these simplifications: + + serialization: + - field-name: color + alias: colour + - name + - convert-from-primitive: color + + serialization: + name: {} + color: { alias: colour, description: "The color of the shape", constraint: required } + colour: { type: convert-from-primitive } + +This works because we've defined the following sets of rules for serializing serializations: + +``` +- field-name: serialization + field-type: list + serialization: + + # given `- name` rewrite as `- { top-level-field: name }`, which will then be further rewritten + - type: convert-from-primitive + key: top-level-field + + # alternative implementation of above (more explicit, not relying on singleton map conversion) + # e.g. transforms `- name` to `- { type: top-level-field, field-name: name }` + - type: convert-from-primitive + key: field-name + defaults: + type: top-level-field + + # convert-singleton-map allows a key to be promoted as a primary key + # for a more convenient representation, especially when a list is expected; + # in this example `k: { type: x }` becomes `{ field-name: k, type: x}` + # (and same for shorthand `k: x`; however if just `k: {}` is supplied it + # takes a default type `top-level-field`); here it is restricted to being inside + # a larger map so as not to conflict with the next rule + - type: convert-singleton-map + only-in-mode: map + key-for-key: .value # as above, will be converted later + key-for-string-value: type # note, only applies if x non-blank + + # sometimes it is convenient to have lists containing maps with a single key, + # for cases where the above rule might be wanted but the keys would conflict; + # this transforms `- x: k` or `- x: { .value: k }` to `- { type: x, .value: k }` + # (use `only-apply-in` restrictions with care; the example here shows how it + # can quickly become confusing; also note it's handy to use alongside a rule + # `convert-from-primitive` to introduce the same `key`) + - type: convert-singleton-map + only-in-mode: list + key-for-key: type # NB skipped if the value is a map containing this key + # if the value is a map, they will merge + # otherwise the value is set as `.value` for conversion later + + # applies any listed unset default keys to the given default values, + # either on a map, or if a list then for every map entry in the list; + # here this essentially makes `top-level-field` the default type + - type: default-map-values + defaults: + type: top-level-field +``` + +We also rely on `top-level-field` having a rule `rename-default-value: field-name` +and `convert-from-primitive` having a rule `rename-default-value: key` +to convert the `.value` key appropriately for those types. + +This can have some surprising side-effects in edge cases; consider: + +``` + # BAD: this would try to load a type called 'color' + serialization: + - color: {} + # GOOD options + serialization: + - color + # or + serialization: + color: {} + # or + serialization: + color: top-level-field + + # BAD: this would try to load a type called 'field-name' + serialization: + - field-name: color + # GOOD options are those in the previous block or to add another field + serialization: + - field-name: color + alias: colour + + # BAD: this ultimately takes "top-level-field" as the "field-name", giving a conflict + serialization: + top-level-field: { field-name: color } + # GOOD options (in addition to those in previous section, but assuming you wanted to say the type explicitly) + serialization: + - top-level-field: { field-name: color } + # or + - top-level-field: color +``` + +In most cases it's probably a bad idea to do this much overloading! +But here it does the right thing in most cases, and it serves to illustrate the flexibility of this approach. + +The serializer definitions will normally be taken from java annotations and not written by hand, +so emphasis should be on making type definitions easy-to-read (which overloading does nicely), and +instance definitions both easy-to-read and -write, rather than type definitions easy-to-write. + +Of course if you have any doubt, simply use the long-winded syntax: + +``` + serialization: + - type: top-level-field + field-name: color +``` + + +### Accepting maps, including generics + +In some cases the underlying type will be a java Map. The lowest level way of representing a map is +as a list of maps specifying the key and value of each entry, as follows: + +``` +- key: + type: string + value: key1 + value: + type: red-square +- key: + type: string + value: key2 + value: a string +``` + +You can also use a more concise map syntax if keys are strings: + + key1: { type: red-square } + key2: "a string" + +If we have information about the generic types -- supplied e.g. with a type of `map` -- +then coercion will be applied in either of the above syntaxes. + + +### Where the expected type is unknown + +In some instances an expected type may be explicitly `java.lang.Object`, or it may be +unknown (eg due to generics). In these cases if no serialization rules are specified, +we take lists as lists, we take maps as objects if a `type` is defined, we take +primitives when used as keys in a map as those primitives, and we take other primitives +as *types*. This last is to prevent errors. It is usually recommended to ensure that +either an expected type will be known or serialization rules are supplied (or both). + + +### Default serialization + +It is possible to set some serializations to be defaults run before or after a supplied list. +This is useful if for instance you want certain different default behaviours across the board. +Note that if interfacing with the existing defaults you wil need to understand that process +in detail; see implementation notes below. + + +## Behaviors which are **not** supported (yet) + +* multiple references to the same object +* include/exclude if null/empty/default +* controlling deep merge behaviour (currently collisions at keys are not merged) +* preventing fields from being set +* more type overloading, conditionals on patterns and types, setting multiple fields from multiple words +* super-types and abstract types (underlying java of `supertypes` must be assignable from underying java of `type`) +* fields fetched by getters, written by setters +* passing arguments to constructors (besides the single maps) + + +## Serializers reference + +We currently support the following manual serializers, in addition to many which are lower-level built-ins. +These can be set as annotations on the java class, or as serializers parsed and noted in the config, +either on a global or a per-class basis in the registry. + +* `top-level-field` (`@YomlFieldAtTopLevel`) + * means that a field is accepted at the top level (it does not need to be in a `field` block) + +* `all-fields-top-level` (`@YomlAllFieldsAtTopLevel`) + * applies the above to all fields + +* `convert-singleton-map` (`@YomlSingletonMap`) + * reads/writes an item as a single-key map where a field value is the key + * particularly useful when working with a list of items to allow a concise multi-entry map syntax + * defaults `.key` and `.value` facilitate working with `rename-...` serializers + +* `config-map-constructor` (`@YomlConfigMapConstructor`) + * indicates that config key static fields should be scanned and passed in a map to the constructor + +* `convert-from-primitive` (`@YomlFromPrimitive`) + * indicates that a primitive can be used for a complex object if just one non-trivial field is set + +* `rename-key` (`@YomlRenameKey`) + * indicates that a key encountered during read-yaml processing should be renamed + * renamed in the reverse direction when writing + * also `rename-default-key` (`@YomlRenameDefaultKey`) and `rename-default-key` (`@YomlRenameDefaultValue`) + as conveniences for the above when renaming `.key` or `.value` respectively + (which are used in some of the other serializers) + +* `default-map-values` (`@YomlDefaultMapValues`) + * allows default key-value pairs to be added on read and removed on write + +* `type-from-other-field` (`@YomlTypeFromOtherField`) + * indicates that type information for one field can be found in the value of another field + + +## Implementation notes + +The `Yoml` entry point starts by invoking the `YamlConverter` which holds the input and +output objects and instructions in a `YomlContext`, and runs through phases, applying +`Serializer` instances on each phase. + +Each `Serializer` exposes methods to `read`, `write`, and `document`, with the appropriate method +invoked depending on what the `Converter` is doing. A `Serializer` will typically check the phase +and do nothing if it isn't appropriate; or if appropriate, they can: + +* modify the objects in the context +* change the phases (restarting, ending, and/or inserting new ones to follow the current phase) +* add new serializers (e.g. once the type has been discovered, it may bring new serializers) + +In addition, they can use a shared blackboard to store local information and communicate state. +This loosely coupled mechanism gives a lot of flexibility for serializers do the right things in +the right order whilst allowing them to be extended, but care does need to be taken. +(The use of special phases and blackboards makes it easier to control what is done when.) + +The general phases are: + +* `manipulating` (custom serializers, operating directly on the input YAML map) +* `handling-type` (default to instantiate the java type, on read, or set the `type` field, on write), + on read, sets the Java object and sets YamlKeysOnBlackboard which are subsequently used for manipulation; + on write, sets the YAML object and sets JavaFieldsOnBlackboard (and sets ReadingTypeOnBlackboard with errors); + inserting new phases: + * when reading: + * `manipulating` (custom serializers again, now with the object created, fields known, and other serializers loaded) + * `handling-fields` (write the fields to the java object) + * and when writing: + * `handling-fields` (collect the fields to write from the java object) + * `manipulating` (custom serializers again, now with the type set and other serializers loaded) + +Afterwards, a completion check runs across all blackboard items to confirm everything has been used +and to enable the most appropriate error to be returned to the user if there are any problems. + + + + +### TODO + +* infinite loop detection: in serialize loop +* handle references, solve infinite loop detection in self-referential writes, with `.reference: ../../OBJ` +* best-serialization vs first-serialization + +* documentation +* yaml segment information and code-point completion + + + +## Old notes + +### Draft Use Case: An Init.d-style entity/effector language + +``` +- id: print-all + type: initdish-effector + steps: + 00-provision: provision + 10-install: + bash: | + curl blah + tar blah + 20-run: + effector: + launch: + parameters... + 21-run-other: + type; invoke-effector + effector: launch + parameters: + ... +``` + + +### Alternate serialization approach (ignore) + +(relying on sequencing and lots of defaults) + +If the `serialization` field (which expects a list) is given a map, the `convert-singleton-map` +serializer converts each pair in that map to a list entry as follows: + +* if V is a map, then the corresponding list entry is the map V with `{ .key: K }` added +* otherwise, the corresponding list entry is `{ .key: K, .value: V }` + +Next, each entry in the list is interpreted as a `serialization` instance, +and the serializations defined for that type specify: + +* If the key `.value` is present and `type` is not defined, that key is renamed to `type` (ignored if `type` is already present) + (code `rename-default-value: type`, handling `{ color: top-level-field }`) +* If the key `.key` is present and `.value` is not defined, that key is renamed to `.value` +* If it is a map of size exactly one, it is converted to a map with `convert-singleton-map` above, and phases not restarted + (code `convert-singleton-maps-in-list`, handling `[ { top-level-field: color } ]`) +* If the key `.key` is present and `type` is not defined, that key is renamed to `type` +* If the item is a primitive V, it is converted to `{ .value: V }`, and phases not restarted +* If it is a map with no `type` defined, `type: top-level-field` is added + +This allows the serialization rules defined on the specific type to kick in to handle `.key` or `.value` entries +introduced but not removed. In the case of `top-level-field` (the default type, as shown in the rules above), +this will rename either such key `.value` to `field-name` (and give an error if `field-name` is already present). + diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/guava/IfFunctionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/guava/IfFunctionsTest.java index 7e140606ca..5d25351271 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/guava/IfFunctionsTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/guava/IfFunctionsTest.java @@ -39,6 +39,7 @@ public void testNoBuilder() { checkTF(IfFunctions.ifEquals(false).value("F").ifEquals(true).value("T").defaultValue("?"), "?"); } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testPredicateAndSupplier() { // we cannot use checkTF here as an IntelliJ issues causes the project to fail to launch as IntelliJ does not diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java index 3c628a9b1e..6a53d682d5 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/javalang/ReflectionsTest.java @@ -24,9 +24,15 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.javalang.coerce.CommonAdaptorTypeCoercions; import org.apache.brooklyn.util.javalang.coerce.TypeCoercer; import org.apache.brooklyn.util.javalang.coerce.TypeCoercerExtensible; @@ -132,6 +138,12 @@ public void testInvocation() throws Exception { Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3, 4, 5)).get(), "hello12"); } + @Test + public void testFindConstructors() throws Exception { + Asserts.assertPresent(Reflections.findConstructorExactMaybe(String.class, String.class)); + Asserts.assertNotPresent(Reflections.findConstructorExactMaybe(String.class, Object.class)); + } + @Test public void testInvocationCoercingArgs() throws Exception { TypeCoercerExtensible rawCoercer = TypeCoercerExtensible.newDefault(); @@ -252,4 +264,150 @@ protected void otherProtectedMethod() {} public static void otherStaticMethod() {} } + + public static class FF1 { + int y; + public int x; + public FF1(int x, int y) { this.x = x; this.y = y; } + } + public static class FF2 extends FF1 { + public int z; + int x; + public FF2(int x, int y, int x2, int z) { super(x, y); this.x = x2; this.z = z; } + } + + @Test + public void testFindPublicFields() throws Exception { + List fields = Reflections.findPublicFieldsOrderedBySuper(FF2.class); + if (fields.size() != 2) Assert.fail("Wrong number of fields: "+fields); + int i=0; + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "z"); + } + + @Test + public void testFindAllFields() throws Exception { + List fields = Reflections.findFields(FF2.class, null, null); + // defaults to SUB_BEST_FIELD_LAST_THEN_ALPHA + if (fields.size() != 4) Assert.fail("Wrong number of fields: "+fields); + int i=0; + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "y"); + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "z"); + } + + @Test + public void testFindAllFieldsSubBestFirstThenAlpha() throws Exception { + List fields = Reflections.findFields(FF2.class, null, FieldOrderings.SUB_BEST_FIELD_FIRST_THEN_ALPHABETICAL); + if (fields.size() != 4) Assert.fail("Wrong number of fields: "+fields); + int i=0; + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "z"); + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "y"); + } + + @Test + public void testFindAllFieldsSubBestLastThenAlpha() throws Exception { + List fields = Reflections.findFields(FF2.class, null, FieldOrderings.SUB_BEST_FIELD_LAST_THEN_ALPHABETICAL); + if (fields.size() != 4) Assert.fail("Wrong number of fields: "+fields); + int i=0; + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "y"); + Assert.assertEquals(fields.get(i++).getName(), "x"); + Assert.assertEquals(fields.get(i++).getName(), "z"); + } + + @Test + public void testFindAllFieldsAlphaSubBestFirst() throws Exception { + List fields = Reflections.findFields(FF2.class, null, FieldOrderings.ALPHABETICAL_FIELD_THEN_SUB_BEST_FIRST); + if (fields.size() != 4) Assert.fail("Wrong number of fields: "+fields); + int i=0; + Assert.assertEquals(fields.get(i).getName(), "x"); + Assert.assertEquals(fields.get(i++).getDeclaringClass(), FF2.class); + Assert.assertEquals(fields.get(i).getName(), "x"); + Assert.assertEquals(fields.get(i++).getDeclaringClass(), FF1.class); + Assert.assertEquals(fields.get(i++).getName(), "y"); + Assert.assertEquals(fields.get(i++).getName(), "z"); + } + + @Test + public void testFindAllFieldsNotAlpha() throws Exception { + // ?? - does this test depend on the JVM? it preserves the default order of fields + List fields = Reflections.findFields(FF2.class, null, FieldOrderings.SUB_BEST_FIELD_LAST_THEN_DEFAULT); + if (fields.size() != 4) Assert.fail("Wrong number of fields: "+fields); + int i=0; + // can't say more about order than this + Assert.assertEquals(MutableSet.of(fields.get(i++).getName(), fields.get(i++).getName()), + MutableSet.of("x", "y")); + Assert.assertEquals(MutableSet.of(fields.get(i++).getName(), fields.get(i++).getName()), + MutableSet.of("x", "z")); + } + + @Test + public void testFindField() throws Exception { + FF2 f2 = new FF2(1,2,3,4); + Field fz = Reflections.findField(FF2.class, "z"); + Assert.assertEquals(fz.get(f2), 4); + Field fx2 = Reflections.findField(FF2.class, "x"); + Assert.assertEquals(fx2.get(f2), 3); + Field fy = Reflections.findField(FF2.class, "y"); + Assert.assertEquals(fy.get(f2), 2); + Field fx1 = Reflections.findField(FF1.class, "x"); + Assert.assertEquals(fx1.get(f2), 1); + + Field fxC2 = Reflections.findField(FF2.class, FF2.class.getCanonicalName()+"."+"x"); + Assert.assertEquals(fxC2.get(f2), 3); + Field fxC1 = Reflections.findField(FF2.class, FF1.class.getCanonicalName()+"."+"x"); + Assert.assertEquals(fxC1.get(f2), 1); + } + + @Test + public void testGetFieldValue() { + FF2 f2 = new FF2(1,2,3,4); + Assert.assertEquals(Reflections.getFieldValueMaybe(f2, "x").get(), 3); + Assert.assertEquals(Reflections.getFieldValueMaybe(f2, "y").get(), 2); + + Assert.assertEquals(Reflections.getFieldValueMaybe(f2, FF2.class.getCanonicalName()+"."+"x").get(), 3); + Assert.assertEquals(Reflections.getFieldValueMaybe(f2, FF1.class.getCanonicalName()+"."+"x").get(), 1); + } + + @SuppressWarnings("rawtypes") + static class MM1 { + public void foo(List l) {} + @SuppressWarnings("unused") + private void bar(List l) {} + } + + @SuppressWarnings("rawtypes") + static class MM2 extends MM1 { + public void foo(ArrayList l) {} + } + + @Test + public void testFindMethods() { + Asserts.assertSize(Reflections.findMethodsCompatible(MM2.class, "foo", ArrayList.class), 2); + Asserts.assertSize(Reflections.findMethodsCompatible(MM2.class, "foo", List.class), 1); + Asserts.assertSize(Reflections.findMethodsCompatible(MM2.class, "foo", Object.class), 0); + Asserts.assertSize(Reflections.findMethodsCompatible(MM2.class, "foo", Map.class), 0); + Asserts.assertSize(Reflections.findMethodsCompatible(MM2.class, "bar", List.class), 1); + Asserts.assertSize(Reflections.findMethodsCompatible(MM1.class, "bar", ArrayList.class), 1); + } + + @Test + public void testFindMethod() { + Asserts.assertTrue(Reflections.findMethodMaybe(MM2.class, "foo", ArrayList.class).isPresent()); + Asserts.assertTrue(Reflections.findMethodMaybe(MM2.class, "foo", List.class).isPresent()); + Asserts.assertTrue(Reflections.findMethodMaybe(MM2.class, "foo", Object.class).isAbsent()); + Asserts.assertTrue(Reflections.findMethodMaybe(MM2.class, "bar", List.class).isPresent()); + Asserts.assertTrue(Reflections.findMethodMaybe(MM2.class, "bar", ArrayList.class).isAbsent()); + } + + @Test + public void testHasSerializableMethods() { + Asserts.assertFalse(Reflections.hasSpecialSerializationMethods(MM2.class)); + Asserts.assertTrue(Reflections.hasSpecialSerializationMethods(LinkedHashMap.class)); + } + } diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/time/DurationTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/time/DurationTest.java index 1296f46d1c..0030b3ef9b 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/time/DurationTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/time/DurationTest.java @@ -20,7 +20,6 @@ import java.util.concurrent.TimeUnit; -import org.apache.brooklyn.util.time.Duration; import org.testng.Assert; import org.testng.annotations.Test; @@ -41,8 +40,8 @@ public void testAdd() { public void testStatics() { Assert.assertEquals((((4*60+3)*60)+30)*1000, - Duration.ONE_MINUTE.times(3). - add(Duration.ONE_HOUR.times(4)). + Duration.ONE_MINUTE.multiply(3). + add(Duration.ONE_HOUR.multiply(4)). add(Duration.THIRTY_SECONDS). toMilliseconds()); } @@ -105,4 +104,17 @@ public void testComparison() { Assert.assertFalse(Duration.seconds(-1).isLongerThan(Duration.ZERO)); } + public void testForevers() { + assertForever("forever"); + assertForever("practically-forever"); + assertForever("a very long time"); + assertForever("a very, very long, long time"); + assertForever("longtime"); + assertForever("very-long"); + } + + protected void assertForever(String name) { + Assert.assertEquals(Duration.PRACTICALLY_FOREVER, Duration.of(name)); + } + } diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/examples/real/DurationYomlTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/examples/real/DurationYomlTests.java new file mode 100644 index 0000000000..12e4cb3b7d --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/examples/real/DurationYomlTests.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.examples.real; + +import org.apache.brooklyn.util.time.Duration; +import org.apache.brooklyn.util.yoml.tests.YomlTestFixture; +import org.testng.annotations.Test; + +public class DurationYomlTests { + + YomlTestFixture y = YomlTestFixture.newInstance(). + addTypeWithAnnotations(Duration.class); + + @Test + public void testReadBasic() { + y.read("{ nanos: 1000000000 }", "duration") + .assertResult(Duration.ONE_SECOND); + } + + @Test + public void testReadNice() { + y.read("1s", "duration") + .assertResult(Duration.ONE_SECOND); + } + + @Test + public void testOneSecond() { + System.out.println("x:"+Duration.ONE_SECOND.toString()); + y.write(Duration.ONE_SECOND, "duration") + .readLastWrite().assertLastsMatch() + .assertLastWriteIgnoringQuotes("1s"); + } + + @Test + public void testZero() { + y.write(Duration.ZERO, "duration") + .readLastWrite().assertLastsMatch() + .assertLastWriteIgnoringQuotes("0ms"); + } + + @Test + public void testForever() { + y.write(Duration.PRACTICALLY_FOREVER, "duration") + .readLastWrite().assertLastsMatch() + .assertLastWriteIgnoringQuotes(Duration.PRACTICALLY_FOREVER_NAME); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConstructionInstructionsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConstructionInstructionsTest.java new file mode 100644 index 0000000000..44ead18941 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConstructionInstructionsTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ConstructionInstructionsTest { + + @Test + public void testBasicNoArgs() { + ConstructionInstruction c1 = ConstructionInstructions.Factory.newDefault(String.class, null); + Assert.assertEquals(c1.create().get(), ""); + } + + @Test + public void testBasicWithArgs() { + ConstructionInstruction c1 = ConstructionInstructions.Factory.newUsingConstructorWithArgs(String.class, MutableList.of("x"), null); + Assert.assertEquals(c1.create().get(), "x"); + } + + @Test + public void testBasicArgsFromWrapper() { + ConstructionInstruction c1 = ConstructionInstructions.Factory.newDefault(String.class, null); + ConstructionInstruction c2 = ConstructionInstructions.Factory.newUsingConstructorWithArgs(null, MutableList.of("x"), c1); + Assert.assertEquals(c2.create().get(), "x"); + } + + @Test + public void testBasicArgsWrapped() { + ConstructionInstruction c1 = ConstructionInstructions.Factory.newUsingConstructorWithArgs(null, MutableList.of("x"), null); + ConstructionInstruction c2 = ConstructionInstructions.Factory.newDefault(String.class, c1); + Assert.assertEquals(c2.create().get(), "x"); + } + + @Test + public void testBasicArgsOverwrite() { + ConstructionInstruction c1 = ConstructionInstructions.Factory.newUsingConstructorWithArgs(String.class, MutableList.of("x"), null); + ConstructionInstruction c2 = ConstructionInstructions.Factory.newUsingConstructorWithArgs(null, MutableList.of("y"), c1); + Assert.assertEquals(c2.create().get(), "x"); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertFromPrimitiveTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertFromPrimitiveTests.java new file mode 100644 index 0000000000..bef29daff3 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertFromPrimitiveTests.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.DefaultKeyValue; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlFromPrimitive; +import org.apache.brooklyn.util.yoml.serializers.AllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.serializers.ConvertFromPrimitive; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.Shape; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.ShapeWithSize; +import org.testng.annotations.Test; + +public class ConvertFromPrimitiveTests { + + YomlTestFixture y = YomlTestFixture.newInstance(). + addType("shape", Shape.class, MutableList.of( + new ConvertFromPrimitive("name", null), + new AllFieldsTopLevel())); + + @Test + public void testPrimitive() { + y.reading("red-square", "shape").writing(new Shape().name("red-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + + YomlTestFixture y2 = YomlTestFixture.newInstance(). + addType("shape", ShapeWithSize.class, MutableList.of( + new ConvertFromPrimitive("name", MutableMap.of("size", 0)), + new AllFieldsTopLevel())); + + @Test + public void testWithDefaults() { + y2.reading("red-square", "shape").writing(new ShapeWithSize().name("red-square").size(0), "shape") + .doReadWriteAssertingJsonMatch(); + } + + + @YomlAllFieldsTopLevel + @YomlFromPrimitive(keyToInsert="name", defaults={@DefaultKeyValue(key="size",val="0",valNeedsParsing=true)}) + static class ShapeAnn extends ShapeWithSize { + } + + YomlTestFixture y3 = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnn.class); + + @Test + public void testFromAnnotation() { + y3.reading("red-square", "shape").writing(new ShapeAnn().name("red-square").size(0), "shape") + .doReadWriteAssertingJsonMatch(); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertSingletonMapTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertSingletonMapTests.java new file mode 100644 index 0000000000..b02f370bae --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/ConvertSingletonMapTests.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.DefaultKeyValue; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlFromPrimitive; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultValue; +import org.apache.brooklyn.util.yoml.annotations.YomlSingletonMap; +import org.apache.brooklyn.util.yoml.serializers.AllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.serializers.ConvertSingletonMap; +import org.apache.brooklyn.util.yoml.serializers.ConvertSingletonMap.SingletonMapMode; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.ShapeWithSize; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.base.Objects; + +public class ConvertSingletonMapTests { + + private static final Logger log = LoggerFactory.getLogger(ConvertSingletonMapTests.class); + + static class ShapeWithTags extends ShapeWithSize { + List tags; + Map metadata; + + @Override + public boolean equals(Object xo) { + return super.equals(xo) && Objects.equal(tags, ((ShapeWithTags)xo).tags) && Objects.equal(metadata, ((ShapeWithTags)xo).metadata); + } + @Override + public int hashCode() { return Objects.hashCode(super.hashCode(), tags, metadata); } + + public ShapeWithTags tags(String ...tags) { this.tags = Arrays.asList(tags); return this; } + public ShapeWithTags metadata(Map metadata) { this.metadata = metadata; return this; } + } + + /* y does: + * * key defaults to name + * * primitive value defaults to color + * * list value defaults to tags + * and y2 adds: + * * map defaults to metadata + * * default for value is size (but won't be used unless one of the above is changed to null) + */ + YomlTestFixture y = YomlTestFixture.newInstance(). + addType("shape", ShapeWithTags.class, MutableList.of( + new AllFieldsTopLevel(), + new ConvertSingletonMap("name", null, "color", "tags", null, null, null, MutableMap.of("size", 0)))); + + YomlTestFixture y2 = YomlTestFixture.newInstance(). + addType("shape", ShapeWithTags.class, MutableList.of( + new AllFieldsTopLevel(), + new ConvertSingletonMap("name", "size", "color", "tags", "metadata", null, null, MutableMap.of("size", 42)))); + + + @Test public void testPrimitiveValue() { + y.reading("{ red-square: red }", "shape").writing(new ShapeWithTags().name("red-square").color("red"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testListValue() { + y.reading("{ good-square: [ good ] }", "shape").writing(new ShapeWithTags().tags("good").name("good-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testMapValueMerge() { + y.reading("{ merge-square: { size: 12 } }", "shape").writing(new ShapeWithTags().size(12).name("merge-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testMapValueSetAndDefaults() { + y2.reading("{ happy-square: { mood: happy } }", "shape").writing(new ShapeWithTags().metadata(MutableMap.of("mood", "happy")).size(42).name("happy-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testMapValueWontMergeIfWouldTreatAsMetadataAndDoesntApplyDefaults() { + y2.reading("{ name: bad-square, size: 12 }", "shape").writing(new ShapeWithTags().size(12).name("bad-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testWontApplyIfTypeUnknown() { + // size is needed without an extra defaults bit + y.write(new ShapeWithTags().name("red-square").color("red"), null) + .assertResult("{ type: shape, color: red, name: red-square, size: 0 }"); + } + + @YomlAllFieldsTopLevel + @YomlSingletonMap(keyForKey="name", keyForListValue="tags", keyForPrimitiveValue="color", + defaults={@DefaultKeyValue(key="size", val="0", valNeedsParsing=true)}) + static class ShapeAnn extends ShapeWithTags {} + + YomlTestFixture y3 = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnn.class); + + @Test public void testAnnPrimitiveValue() { + y3.reading("{ red-square: red }", "shape").writing(new ShapeAnn().name("red-square").color("red"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testAnnListValue() { + y3.reading("{ good-square: [ good ] }", "shape").writing(new ShapeAnn().tags("good").name("good-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testAnnMapValueMerge() { + y3.reading("{ merge-square: { size: 12 } }", "shape").writing(new ShapeAnn().size(12).name("merge-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testAnnNothingExtra() { + y3.reading("{ merge-square: { } }", "shape").writing(new ShapeAnn().name("merge-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testAnnList() { + y3.reading("[ { one: { size: 1 } }, { two: { size: 2 } } ]", "list").writing( + MutableList.of(new ShapeAnn().name("one").size(1), new ShapeAnn().name("two").size(2)), "list") + .doReadWriteAssertingJsonMatch(); + } + + @Test public void testAnnListCompressed() { + // read list-as-map, will write out as list-as-list, and can read that back too + List obj = MutableList.of(new ShapeAnn().name("one").size(1), new ShapeAnn().name("two").size(2)); + y3.read("{ one: { size: 1 }, two: { size: 2 } }", "list").assertResult(obj); + y3.reading("[ { one: { size: 1 } }, { two: { size: 2 } } ]", "list") + .writing(obj, "list") + .doReadWriteAssertingJsonMatch(); + } + + /* perverse example where we parse differently depending whether it is a list or a map */ + YomlTestFixture y4 = YomlTestFixture.newInstance(). + addType("shape", ShapeAnn.class, MutableList.of( + new AllFieldsTopLevel(), + // in map, we take : + new ConvertSingletonMap("name", null, "color", null, null, + MutableList.of(SingletonMapMode.LIST_AS_MAP), null, MutableMap.of("size", 0)), + // in list, we take : + new ConvertSingletonMap("color", null, "name", null, null, + MutableList.of(SingletonMapMode.LIST_AS_LIST), null, MutableMap.of("size", 0)) ) + ); + + List LIST_OF_BLUE_SHAPE = MutableList.of(new ShapeAnn().name("blue_shape").color("blue")); + + @Test public void testAnnListPerverseOrders() { + // read list-as-map, will write out as list-as-list, and can read that back too + String listJson = "[ { blue: blue_shape } ]"; + + y4.read("{ blue_shape: blue }", "list").assertResult(LIST_OF_BLUE_SHAPE); + y4.write(y4.lastReadResult, "list").assertResult(listJson); + y4.read(listJson, "list").assertResult(LIST_OF_BLUE_SHAPE); + } + + @Test public void testAnnDisallowedAtRoot() { + try { + y4.read("{ blue: blue_shape }", "shape"); + Asserts.shouldHaveFailedPreviously("but got "+y4.lastReadResult); + } catch (Exception e) { + log.info("got expected error: "+e); + Asserts.expectedFailureContainsIgnoreCase(e, "blue", "incomplete"); + } + } + + @YomlSingletonMap + @YomlRenameDefaultKey("type") + public static class ShapesAbstract { + } + + @Alias("shapes") + @YomlAllFieldsTopLevel + @YomlFromPrimitive + @YomlRenameDefaultValue("shapes") + public static class Shapes { + List shapes; + } + + @Test public void testComplexListValue() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations(Shapes.class) + .addTypeWithAnnotations(ShapeAnn.class); + y.read("[ {blue_shape: blue} ]", "shapes"); + + Asserts.assertInstanceOf(y.lastReadResult, Shapes.class); + Asserts.assertEquals( ((Shapes)y.lastReadResult).shapes, LIST_OF_BLUE_SHAPE ); + + y.writeLastRead().assertLastsMatch(); + } + + @Test public void testComplexListValueSingletonMapWithType() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations(Shapes.class) + .addTypeWithAnnotations(ShapeAnn.class) + .addTypeWithAnnotations(ShapesAbstract.class); + y.read("shapes: [ {blue_shape: blue} ]", ShapesAbstract.class.getName()); + + Asserts.assertInstanceOf(y.lastReadResult, Shapes.class); + Asserts.assertEquals( ((Shapes)y.lastReadResult).shapes, LIST_OF_BLUE_SHAPE ); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/DefaultMapValuesTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/DefaultMapValuesTests.java new file mode 100644 index 0000000000..fe212c48a6 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/DefaultMapValuesTests.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import org.apache.brooklyn.util.yoml.annotations.DefaultKeyValue; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlDefaultMapValues; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.ShapeWithSize; +import org.testng.annotations.Test; + +public class DefaultMapValuesTests { + + @YomlAllFieldsTopLevel + @YomlDefaultMapValues({@DefaultKeyValue(key="size",val="0",valNeedsParsing=true), + @DefaultKeyValue(key="name",val="anonymous")}) + static class ShapeAnn extends ShapeWithSize { + } + + YomlTestFixture y = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnn.class); + + @Test + public void testEntirelyFromAnnotation() { + y.reading("{ }", "shape").writing(new ShapeAnn().name("anonymous").size(0), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @Test + public void testOverwritingDefault() { + y.reading("{ name: special }", "shape").writing(new ShapeAnn().name("special").size(0), "shape") + .doReadWriteAssertingJsonMatch(); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/FieldTypeFromOtherFieldTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/FieldTypeFromOtherFieldTest.java new file mode 100644 index 0000000000..9f52070459 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/FieldTypeFromOtherFieldTest.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Map; + +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; +import org.apache.brooklyn.util.yoml.annotations.YomlTypeFromOtherField; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Objects; + +/** Tests that top-level fields can be set at the outer level in yaml. */ +public class FieldTypeFromOtherFieldTest { + + @YomlAllFieldsTopLevel + static abstract class FieldTypeFromOtherAbstract { + + public abstract Object val(); + + @Override + public int hashCode() { + return Objects.hashCode(val()); + } + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FieldTypeFromOtherAbstract)) return false; + return Objects.equal(val(), ((FieldTypeFromOtherAbstract)obj).val()); + } + } + + + @Alias("fto") + static class FieldTypeFromOther extends FieldTypeFromOtherAbstract { + + public FieldTypeFromOther(String typ, Object val) { + this.typ = typ; + this.val = val; + } + public FieldTypeFromOther() {} + + String typ; + + @YomlTypeFromOtherField("typ") + Object val; + + @Override + public Object val() { + return val; + } + } + + protected FieldTypeFromOther read(String input) { + YomlTestFixture y = fixture(); + y.read(input, "fto" ); + Asserts.assertInstanceOf(y.lastReadResult, FieldTypeFromOther.class); + return (FieldTypeFromOther)y.lastReadResult; + } + + protected YomlTestFixture fixture() { + return YomlTestFixture.newInstance().addTypeWithAnnotations(FieldTypeFromOther.class); + } + + @Test + public void testValueFromMap() { + Assert.assertEquals(read("{ val: { type: int, value: 42 } }").val, 42); + } + + @Test + public void testTypeUsed() { + Assert.assertEquals(read("{ typ: int, val: 42 }").val, 42); + } + + @Test + public void testReadWriteWithType() { + fixture().reading("{ type: fto, typ: int, val: 42 }").writing(new FieldTypeFromOther("int", 42)).doReadWriteAssertingJsonMatch(); + } + + @Test + public void testReadWriteWithoutType() { + fixture().reading("{ type: fto, val: { type: int, value: 42 } }").writing(new FieldTypeFromOther(null, 42)).doReadWriteAssertingJsonMatch(); + } + + @Test + public void testFailsIfTypeUnknown() { + try { + FieldTypeFromOther result = read("{ val: 42 }"); + Asserts.shouldHaveFailedPreviously("Instead got "+result); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "val"); + } + } + + @Test + public void testMapSupportedWithType() { + Assert.assertEquals(read("{ typ: int, val: { type: int, value: 42 } }").val, 42); + } + + @Alias("fto-type-key-not-real") + static class FieldTypeFromOtherNotReal extends FieldTypeFromOtherAbstract { + + public FieldTypeFromOtherNotReal(Object val) { + this.val = val; + } + public FieldTypeFromOtherNotReal() {} + + @YomlTypeFromOtherField(value="typ", real=false) + Object val; + + @Override + public Object val() { + return val; + } + } + + @Test + public void testReadWriteWithTypeInNotRealKey() { + // in this mode the field is in yaml but not on the object + YomlTestFixture.newInstance().addTypeWithAnnotations(FieldTypeFromOtherNotReal.class) + .reading("{ type: fto-type-key-not-real, typ: int, val: 42 }").writing(new FieldTypeFromOtherNotReal(42)).doReadWriteAssertingJsonMatch(); + } + + + @YomlConfigMapConstructor("vals") + @Alias("fto-from-config") + static class FieldTypeFromOtherConfig extends FieldTypeFromOtherAbstract { + + private Map vals; + + @YomlTypeFromOtherField(value="valType") + public static final ConfigKey VALUE = new TopLevelConfigKeysTests.MockConfigKey(Object.class, "val"); + + @Alias(preferred="typ") + public static final ConfigKey TYPE = new TopLevelConfigKeysTests.MockConfigKey(String.class, "valType"); + + public FieldTypeFromOtherConfig(Map vals) { + this.vals = vals; + } + + @Override + public Object val() { + return vals.get(VALUE.getName()); + } + } + + @Test + public void testReadWriteWithTypeInConfig() { + YomlTestFixture.newInstance().addTypeWithAnnotations(FieldTypeFromOtherConfig.class) + .reading("{ type: fto-from-config, typ: int, val: 42 }").writing(new FieldTypeFromOtherConfig( + MutableMap.of("valType", (Object)"int", "val", 42))) + .doReadWriteAssertingJsonMatch(); + } + + + @YomlConfigMapConstructor("vals") + @Alias("fto-from-config-type-not-real") + static class FieldTypeFromOtherConfigTypeNotReal extends FieldTypeFromOtherAbstract { + + private Map vals; + + @YomlTypeFromOtherField(value="typ", real=false) + public static final ConfigKey VALUE = new TopLevelConfigKeysTests.MockConfigKey(Object.class, "val"); + + public FieldTypeFromOtherConfigTypeNotReal(Map vals) { + this.vals = vals; + } + + @Override + public Object val() { + return vals.get(VALUE.getName()); + } + } + + @Test + public void testReadWriteWithTypeInConfigTypeNotReal() { + YomlTestFixture.newInstance().addTypeWithAnnotations(FieldTypeFromOtherConfigTypeNotReal.class) + .reading("{ type: fto-from-config-type-not-real, typ: int, val: 42 }").writing(new FieldTypeFromOtherConfigTypeNotReal( + MutableMap.of("val", (Object)42))) + .doReadWriteAssertingJsonMatch(); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/MockYomlTypeRegistry.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/MockYomlTypeRegistry.java new file mode 100644 index 0000000000..03c53e6be9 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/MockYomlTypeRegistry.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.javalang.Boxing; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yaml.Yamls; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.YomlTypeRegistry; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstruction; +import org.apache.brooklyn.util.yoml.internal.ConstructionInstructions; +import org.apache.brooklyn.util.yoml.internal.YomlContext; +import org.apache.brooklyn.util.yoml.internal.YomlContextForRead; +import org.apache.brooklyn.util.yoml.internal.YomlConverter; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; + +import com.google.common.collect.Iterables; + +public class MockYomlTypeRegistry implements YomlTypeRegistry { + + static class MockRegisteredType { + final String id; + final String parentType; + final Set interfaceTypes; + + final Class javaType; + final Collection serializers; + final Object yamlDefinition; + + public MockRegisteredType(String id, String parentType, Class javaType, Collection interfaceTypes, Collection serializers, Object yamlDefinition) { + super(); + this.id = id; + this.parentType = parentType; + this.javaType = javaType; + this.interfaceTypes = MutableSet.copyOf(interfaceTypes); + this.serializers = MutableList.copyOf(serializers); + this.yamlDefinition = yamlDefinition; + } + } + + Map types = MutableMap.of(); + + @Override + public Object newInstance(String typeName, Yoml yoml) { + return newInstanceMaybe(typeName, yoml).get(); + } + @Override + public Maybe newInstanceMaybe(String typeName, Yoml yoml) { + return newInstanceMaybe(typeName, yoml, null); + } + + @Override + public Maybe newInstanceMaybe(String typeName, Yoml yoml, @Nullable YomlContext yomlContext) { + MockRegisteredType type = types.get(typeName); + if (type!=null && type.yamlDefinition!=null) { + String parentTypeName = type.parentType; + if (type.parentType==null && type.javaType!=null) parentTypeName = getDefaultTypeNameOfClass(type.javaType); + return Maybe.of(new YomlConverter(yoml.getConfig()).read( new YomlContextForRead(type.yamlDefinition, "", parentTypeName, null).constructionInstruction(yomlContext.getConstructionInstruction()) )); + // have to do the above instead of below to ensure construction instruction is passed + // return Maybe.of(yoml.readFromYamlObject(type.yamlDefinition, parentTypeName)); + } + + Maybe> javaType = getJavaTypeInternal(type, typeName); + ConstructionInstruction constructor = yomlContext==null ? null : yomlContext.getConstructionInstruction(); + if (javaType.isAbsent() && constructor==null) { + if (type==null) return Maybe.absent("Unknown type `"+typeName+"`"); + return Maybe.absent(new IllegalStateException("Incomplete hierarchy for "+type, ((Maybe.Absent)javaType).getException())); + } + + return ConstructionInstructions.Factory.newDefault(javaType.get(), constructor).create(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected static Maybe> maybeClass(Class clazz) { + // restrict unchecked wildcard generic warning/suppression to here + return (Maybe) Maybe.ofDisallowingNull(clazz); + } + + @Override + public Maybe> getJavaTypeMaybe(String typeName, @Nullable YomlContext contextIgnoredInMock) { + if (typeName==null) return Maybe.absent(); + // strip generics here + if (typeName.indexOf('<')>0) typeName = typeName.substring(0, typeName.indexOf('<')); + return getJavaTypeInternal(types.get(typeName), typeName); + } + + protected Maybe> getJavaTypeInternal(MockRegisteredType registeredType, String typeName) { + Maybe> result = Maybe.absent(); + + if (result.isAbsent() && registeredType!=null) result = maybeClass(registeredType.javaType); + if (result.isAbsent() && registeredType!=null) result = getJavaTypeMaybe(registeredType.parentType, null); + + if (result.isAbsent()) { + result = Maybe.absent("Unknown type '"+typeName+"' (no match available in mock library)"); + if (typeName==null) return result; + } + + if (result.isAbsent()) result = maybeClass(Boxing.boxedType(Boxing.getPrimitiveType(typeName).orNull())).or(result); + if (result.isAbsent() && YomlUtils.TYPE_STRING.equals(typeName)) result = maybeClass(String.class); + + if (result.isAbsent() && typeName.startsWith("java:")) { + typeName = Strings.removeFromStart(typeName, "java:"); + try { + // IRL use injected loader + result = maybeClass(Class.forName(typeName)); + } catch (ClassNotFoundException e) { + // ignore, this isn't a java type + } + } + return result; + } + + /** simplest type def -- an alias for a java class */ + public void put(String typeName, Class javaType) { + put(typeName, javaType, null); + } + public void put(String typeName, Class javaType, Collection serializers) { + types.put(typeName, new MockRegisteredType(typeName, "java:"+javaType.getName(), javaType, MutableSet.of(), serializers, null)); + } + + /** takes a simplified yaml definition supporting a map with a key `type` and optionally other keys */ + public void put(String typeName, String yamlDefinition) { + put(typeName, yamlDefinition, null); + } + @SuppressWarnings("unchecked") + public void put(String typeName, String yamlDefinition, List serializers) { + Object yamlObject = Iterables.getOnlyElement( Yamls.parseAll(yamlDefinition) ); + if (!(yamlObject instanceof Map)) throw new IllegalArgumentException("Mock only supports map definitions"); + + Object type = ((Map)yamlObject).remove("type"); + if (!(type instanceof String)) throw new IllegalArgumentException("Mock requires key `type` with string value"); + + Maybe> javaType = getJavaTypeMaybe((String)type, null); + if (javaType.isAbsent()) throw new IllegalArgumentException("Mock cannot resolve parent type `"+type+"` in definition of `"+typeName+"`: "+ + ((Maybe.Absent)javaType).getException()); + + Object interfaceTypes = ((Map)yamlObject).remove("interfaceTypes"); + if (((Map)yamlObject).isEmpty()) yamlObject = null; + + types.put(typeName, new MockRegisteredType(typeName, (String)type, javaType.get(), (Collection)interfaceTypes, serializers, yamlObject)); + } + + @Override + public String getTypeName(Object obj) { + return getTypeNameOfClass(obj.getClass()); + } + + @Override + public String getTypeNameOfClass(Class type) { + if (type==null) return null; + for (Map.Entry t: types.entrySet()) { + if (type.equals(t.getValue().javaType) && t.getValue().yamlDefinition==null) return t.getKey(); + } + return getDefaultTypeNameOfClass(type); + } + + protected String getDefaultTypeNameOfClass(Class type) { + Maybe primitive = Boxing.getPrimitiveName(type); + if (primitive.isPresent()) return primitive.get(); + if (String.class.equals(type)) return "string"; + // map and list handled by those serializers + return "java:"+type.getName(); + } + + @Override + public Iterable getSerializersForType(String typeName, YomlContext context) { + Set result = MutableSet.of(); + collectSerializers(typeName, result, MutableSet.of()); + return result; + } + + protected void collectSerializers(String typeName, Collection serializers, Set typesVisited) { + if (typeName==null || !typesVisited.add(typeName)) return; + MockRegisteredType rt = types.get(typeName); + if (rt==null) return; + if (rt.serializers!=null) serializers.addAll(rt.serializers); + if (rt.parentType!=null) { + collectSerializers(rt.parentType, serializers, typesVisited); + } + if (rt.interfaceTypes!=null) { + for (String interfaceType: rt.interfaceTypes) { + collectSerializers(interfaceType, serializers, typesVisited); + } + } + } +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/RenameKeyTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/RenameKeyTests.java new file mode 100644 index 0000000000..8d9b2d1b29 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/RenameKeyTests.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import org.apache.brooklyn.util.yoml.annotations.DefaultKeyValue; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlFromPrimitive; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultKey; +import org.apache.brooklyn.util.yoml.annotations.YomlRenameKey.YomlRenameDefaultValue; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.Shape; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.ShapeWithSize; +import org.testng.annotations.Test; + +public class RenameKeyTests { + + @YomlAllFieldsTopLevel + @YomlRenameKey(oldKeyName="name-of-tiny-shape", newKeyName="name", defaults={@DefaultKeyValue(key="size",val="0",valNeedsParsing=true)}) + static class ShapeAnn extends ShapeWithSize { + } + + YomlTestFixture y = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnn.class); + + @Test + public void testFromAnnotation() { + y.reading("{ name-of-tiny-shape: red-square }", "shape") + .writing(new ShapeAnn().name("red-square").size(0), "shape") + .doReadWriteAssertingJsonMatch(); + } + + @YomlAllFieldsTopLevel + @YomlFromPrimitive + @YomlRenameDefaultValue("name") + static class ShapeAnnUsingDotValue extends Shape { + } + + YomlTestFixture y2 = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnnUsingDotValue.class); + + @Test + public void testWithDotValue() { + y2.reading("red-square", "shape") + .writing(new ShapeAnnUsingDotValue().name("red-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + + + @YomlAllFieldsTopLevel + @YomlRenameDefaultKey("name") + static class ShapeAnnUsingDotKey extends Shape { + } + + YomlTestFixture y3 = YomlTestFixture.newInstance(). + addTypeWithAnnotations("shape", ShapeAnnUsingDotKey.class); + + @Test + public void testWithDotKey() { + y3.reading("{ .key: red-square }", "shape") + .writing(new ShapeAnnUsingDotKey().name("red-square"), "shape") + .doReadWriteAssertingJsonMatch(); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelConfigKeysTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelConfigKeysTests.java new file mode 100644 index 0000000000..f3226b6c89 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelConfigKeysTests.java @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +import org.apache.brooklyn.config.ConfigInheritance; +import org.apache.brooklyn.config.ConfigInheritance.ConfigInheritanceContext; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistryUsingConfigMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Predicate; +import com.google.common.reflect.TypeToken; + +/** Tests that the default serializers can read/write types and fields. + *

+ * And shows how to use them at a low level. + */ +public class TopLevelConfigKeysTests { + + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(TopLevelConfigKeysTests.class); + + static class MockConfigKey implements ConfigKey { + String name; + Class type; + TypeToken typeToken; + T defaultValue; + + public MockConfigKey(Class type, String name) { + this.name = name; + this.type = type; + this.typeToken = TypeToken.of(type); + } + + public MockConfigKey(Class type, String name, T defaultValue) { + this.name = name; + this.type = type; + this.typeToken = TypeToken.of(type); + this.defaultValue = defaultValue; + } + + @SuppressWarnings("unchecked") + public MockConfigKey(TypeToken typeToken, String name) { + this.name = name; + this.typeToken = typeToken; + this.type = (Class)typeToken.getRawType(); + } + + @Override public String getDescription() { return null; } + @Override public String getName() { return name; } + @Override public Collection getNameParts() { return MutableList.of(name); } + @Override public TypeToken getTypeToken() { return typeToken; } + @Override public Class getType() { return type; } + + @Override public String getTypeName() { throw new UnsupportedOperationException(); } + @Override public T getDefaultValue() { return defaultValue; } + @Override public boolean hasDefaultValue() { return defaultValue!=null; } + @Override public boolean isReconfigurable() { return false; } + @Override public ConfigInheritance getTypeInheritance() { return null; } + @Override public ConfigInheritance getParentInheritance() { return null; } + @Override public ConfigInheritance getInheritance() { return null; } + @Override public Predicate getConstraint() { return null; } + @Override public boolean isValueValid(T value) { return true; } + @Override public ConfigInheritance getInheritanceByContext(ConfigInheritanceContext context) { return null; } + @Override public Map getInheritanceByContext() { return MutableMap.of(); } + @Override public Collection getDeprecatedNames() { return MutableList.of(); } + } + + static class S1 { + static ConfigKey K1 = new MockConfigKey(String.class, "k1"); + Map keys = MutableMap.of(); + S1(Map keys) { this.keys.putAll(keys); } + + @Override + public boolean equals(Object obj) { + return (obj instanceof S1) && ((S1)obj).keys.equals(keys); + } + @Override + public int hashCode() { + return keys.hashCode(); + } + @Override + public String toString() { + return super.toString()+keys; + } + } + + @Test + public void testRead() { + YomlTestFixture y = YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s1", S1.class, MutableMap.of("keys", "config")); + + y.read("{ type: s1, k1: foo }", null); + + Asserts.assertInstanceOf(y.lastReadResult, S1.class); + Asserts.assertEquals(((S1)y.lastReadResult).keys.get("k1"), "foo"); + } + + @Test + public void testReadExpectingType() { + YomlTestFixture y = YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s1", S1.class, MutableMap.of("keys", "config")); + + y.read("{ k1: foo }", "s1"); + + Asserts.assertInstanceOf(y.lastReadResult, S1.class); + Asserts.assertEquals(((S1)y.lastReadResult).keys.get("k1"), "foo"); + } + + @Test + public void testWrite() { + YomlTestFixture y = YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s1", S1.class, MutableMap.of("keys", "config")); + + S1 s1 = new S1(MutableMap.of("k1", "foo")); + y.write(s1); + YomlTestFixture.assertEqualish(Jsonya.newInstance().add(y.lastWriteResult).toString(), "{ type: s1, k1: foo }", "wrong serialization"); + } + + @Test + public void testReadWrite() { + YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s1", S1.class, MutableMap.of("keys", "config")) + .reading("{ type: s1, k1: foo }").writing(new S1(MutableMap.of("k1", "foo"))) + .doReadWriteAssertingJsonMatch(); + } + + static class S2 extends S1 { + S2(Map keys) { super(keys); } + static ConfigKey K2 = new MockConfigKey(String.class, "k2"); + static ConfigKey KS = new MockConfigKey(S1.class, "ks"); + } + + @Test + public void testReadWriteInherited() { + YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s2", S2.class, MutableMap.of("keys", "config")) + .reading("{ type: s2, k1: foo, k2: bar }").writing(new S2(MutableMap.of("k1", "foo", "k2", "bar"))) + .doReadWriteAssertingJsonMatch(); + } + + @Test + public void testReadWriteNested() { + YomlTestFixture.newInstance() + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s1", S1.class, MutableMap.of("keys", "config")) + .addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance("s2", S2.class, MutableMap.of("keys", "config")) + .reading("{ type: s2, ks: { k1: foo } }").writing(new S2(MutableMap.of("ks", new S1(MutableMap.of("k1", "foo"))))) + .doReadWriteAssertingJsonMatch(); + } + @Test + public void testReadWriteNestedGlobalConfigKeySupport() { + YomlTestFixture y = YomlTestFixture.newInstance(YomlConfig.Builder.builder() + .serializersPostAdd(InstantiateTypeFromRegistryUsingConfigMap.newFactoryIgnoringInheritance().newConfigKeyClassScanningSerializers("keys", "config", true)) + .serializersPostAddDefaults().build()); + y.addTypeWithAnnotations("s1", S1.class) + .addTypeWithAnnotations("s2", S2.class) + .reading("{ type: s2, ks: { k1: foo } }").writing(new S2(MutableMap.of("ks", new S1(MutableMap.of("k1", "foo"))))) + .doReadWriteAssertingJsonMatch(); + } + + @YomlConfigMapConstructor(value="keys", writeAsKey="extraConfig") + static class S3 extends S1 { + S3(Map keys) { super(keys); } + static ConfigKey K2 = new MockConfigKey(String.class, "k2"); + static ConfigKey KS1 = new MockConfigKey(S1.class, "ks1"); + static ConfigKey KS3 = new MockConfigKey(S3.class, "ks3"); + } + + @Test + public void testReadWriteAnnotation() { + YomlTestFixture.newInstance() + .addTypeWithAnnotations("s3", S3.class) + .reading("{ type: s3, ks3: { k1: foo } }").writing(new S3(MutableMap.of("ks3", new S3(MutableMap.of("k1", "foo"))))) + .doReadWriteAssertingJsonMatch(); + } + + @Test + public void testReadWriteAnnotationTypeInfoNeeded() { + YomlTestFixture.newInstance() + .addTypeWithAnnotations("s1", S1.class) + .addTypeWithAnnotations("s3", S3.class) + .reading("{ type: s3, ks1: { type: s3, k1: foo } }").writing(new S3(MutableMap.of("ks1", new S3(MutableMap.of("k1", "foo"))))) + .doReadWriteAssertingJsonMatch(); + } + + @Test + public void testReadWriteExtraField() { + YomlTestFixture.newInstance() + .addTypeWithAnnotations("s3", S3.class) + .reading("{ type: s3, k1: foo, extraConfig: { k0: { type: string, value: bar } } }").writing(new S3(MutableMap.of("k1", "foo", "k0", "bar"))) + .doReadWriteAssertingJsonMatch(); + } + + @YomlConfigMapConstructor("") + @YomlAllFieldsTopLevel + static class KeyAsField { + @Alias("k") + static ConfigKey K1 = new MockConfigKey(String.class, "key1"); + final String key1Field; + transient final Map keysSuppliedToConstructorForTestAssertions; + KeyAsField(Map keys) { + key1Field = (String) keys.get(K1.getName()); + keysSuppliedToConstructorForTestAssertions = keys; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof KeyAsField) && Objects.equals( ((KeyAsField)obj).key1Field, key1Field); + } + @Override + public int hashCode() { + return Objects.hash(key1Field); + } + @Override + public String toString() { + return super.toString()+":"+key1Field; + } + } + + final KeyAsField KF_FOO = new KeyAsField(MutableMap.of("key1", "foo")); + + @Test + public void testNoConfigMapFieldCanReadKeyToMapConstructor() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf", KeyAsField.class); + y.read("{ key1: foo }", "kf").assertResult(KF_FOO); + Assert.assertEquals(((KeyAsField)y.lastReadResult).keysSuppliedToConstructorForTestAssertions, KF_FOO.keysSuppliedToConstructorForTestAssertions); + } + @Test + public void testNoConfigMapFieldCanReadKeyAliasToMapConstructor() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf", KeyAsField.class); + y.read("{ k: foo }", "kf").assertResult(KF_FOO); + Assert.assertEquals(((KeyAsField)y.lastReadResult).keysSuppliedToConstructorForTestAssertions, KF_FOO.keysSuppliedToConstructorForTestAssertions); + } + @Test + public void testStaticFieldNameNotRelevant() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf", KeyAsField.class); + try { + y.read("{ k1: foo }", "kf"); + Asserts.shouldHaveFailedPreviously("Got "+y.lastReadResult); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "k1", "foo"); + } + } + + @Test + public void testNoConfigMapFieldCanWriteAndReadToFieldDirectly() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf", KeyAsField.class); + // writing must write to the field directly because it is not defined how to reverse map to the config key + y.writing(KF_FOO).reading("{ type: kf, key1Field: foo }").doReadWriteAssertingJsonMatch(); + // nothing passed to constructor, but constructor is invoked + Assert.assertEquals(((KeyAsField)y.lastReadResult).keysSuppliedToConstructorForTestAssertions, MutableMap.of(), + "Constructor given unexpectedly non-empty map: "+((KeyAsField)y.lastReadResult).keysSuppliedToConstructorForTestAssertions); + } + + static class KfA2C extends KeyAsField { + KfA2C(Map keys) { super(keys); } + + @Alias("key1Field") // alias same name as field means it *is* passed to constructor + static ConfigKey K1 = new MockConfigKey(String.class, "key1"); + } + + private static final KfA2C KF_A2C_FOO = new KfA2C(MutableMap.of("key1", "foo")); + + @Test + public void testConfigKeyTopLevelInherited() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf-a2c", KfA2C.class); + y.read("{ key1: foo }", "kf-a2c").assertResult(KF_A2C_FOO); + } + + @Test + public void testConfigKeyOverrideHidesParentAlias() { + // this could be weakened to be allowed (but aliases at types must not be, for obvious reasons!) + + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf-a2c", KfA2C.class); + try { + y.read("{ k: foo }", "kf-a2c"); + Asserts.shouldHaveFailedPreviously("Got "+y.lastReadResult); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "k", "foo"); + } + } + + @Test + public void testNoConfigMapFieldWillPreferConstructorIfKeyForFieldCanBeFound() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("kf-a2c", KfA2C.class); + // writing must write to the field because it is not defined how to reverse map to the config key + y.writing(KF_A2C_FOO).reading("{ type: kf-a2c, key1Field: foo }").doReadWriteAssertingJsonMatch(); + // is passed to constructor + Assert.assertEquals(((KeyAsField)y.lastReadResult).keysSuppliedToConstructorForTestAssertions, KF_FOO.keysSuppliedToConstructorForTestAssertions); + } + + + static class SDefault extends S3 { + transient final Map keysSuppliedToConstructorForTestAssertions; + + SDefault(Map keys) { + super(keys); + keysSuppliedToConstructorForTestAssertions = keys; + } + + static ConfigKey KD = new MockConfigKey(String.class, "keyD", "default"); + } + + @Test + public void testConfigKeyDefaultsReadButNotWritten() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("s-default", SDefault.class); + y.reading("{ }", "s-default").writing(new SDefault(MutableMap.of()), "s-default") + .doReadWriteAssertingJsonMatch(); + + Asserts.assertSize( ((SDefault)y.lastReadResult).keysSuppliedToConstructorForTestAssertions.keySet(), 0 ); + } + + @Test + public void testConfigKeyDefaultsReadButNotWrittenWithAtLeastOneConfigValueSupplied() { + // behaviour is different in this case + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("s-default", SDefault.class); + y.reading("{ k2: x }", "s-default").writing(new SDefault(MutableMap.of("k2", "x")), "s-default") + .doReadWriteAssertingJsonMatch(); + + Asserts.assertSize( ((SDefault)y.lastReadResult).keysSuppliedToConstructorForTestAssertions.keySet(), 1 ); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelFieldsTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelFieldsTests.java new file mode 100644 index 0000000000..fcfbfa6e50 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/TopLevelFieldsTests.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.List; +import java.util.Set; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.serializers.AllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.serializers.TopLevelFieldSerializer; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.Shape; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.ShapeWithSize; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** Tests that top-level fields can be set at the outer level in yaml. */ +public class TopLevelFieldsTests { + + public static YomlSerializer topLevelFieldSerializer(String yaml) { + return (YomlSerializer) YomlTestFixture.newInstance().read("{ fields: "+yaml+" }", "java:"+TopLevelFieldSerializer.class.getName()).lastReadResult; + } + + protected static YomlTestFixture simpletopLevelFieldFixture() { + return YomlTestFixture.newInstance(). + addType("shape", Shape.class, MutableList.of(topLevelFieldSerializer("{ fieldName: name }"))); + } + + static String SIMPLE_IN_WITHOUT_TYPE = "{ name: diamond, fields: { color: black } }"; + static Shape SIMPLE_OUT = new Shape().name("diamond").color("black"); + + static String SIMPLE_IN_NAME_ONLY_WITHOUT_TYPE = "{ name: diamond }"; + static Shape SIMPLE_OUT_NAME_ONLY = new Shape().name("diamond"); + + @Test + public void testReadTopLevelField() { + simpletopLevelFieldFixture(). + read( SIMPLE_IN_WITHOUT_TYPE, "shape" ). + assertResult( SIMPLE_OUT ); + } + @Test + public void testWriteTopLevelField() { + simpletopLevelFieldFixture(). + write( SIMPLE_OUT, "shape" ). + assertResult( SIMPLE_IN_WITHOUT_TYPE ); + } + + @Test + public void testReadTopLevelFieldNameOnly() { + simpletopLevelFieldFixture(). + read( SIMPLE_IN_NAME_ONLY_WITHOUT_TYPE, "shape" ). + assertResult( SIMPLE_OUT_NAME_ONLY ); + } + @Test + public void testWriteTopLevelFieldNameOnly() { + simpletopLevelFieldFixture(). + write( SIMPLE_OUT_NAME_ONLY, "shape" ). + assertResult( SIMPLE_IN_NAME_ONLY_WITHOUT_TYPE ); + } + + static String SIMPLE_IN_WITH_TYPE = "{ type: shape, name: diamond, fields: { color: black } }"; + + @Test + public void testReadTopLevelFieldNoExpectedType() { + simpletopLevelFieldFixture(). + read( SIMPLE_IN_WITH_TYPE, null ). + assertResult( SIMPLE_OUT); + } + @Test + public void testWriteTopLevelFieldNoExpectedType() { + simpletopLevelFieldFixture(). + write( SIMPLE_OUT, null ). + assertResult( SIMPLE_IN_WITH_TYPE ); + } + + protected static YomlTestFixture commonTopLevelFieldFixtureKeyNameAlias() { + return commonTopLevelFieldFixtureKeyNameAlias(""); + } + protected static YomlTestFixture commonTopLevelFieldFixtureKeyNameAlias(String extra) { + return YomlTestFixture.newInstance(). + addType("shape", Shape.class, MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-name, alias: my-name"+extra+" }"))); + } + + static String COMMON_IN_KEY_NAME = "{ shape-name: diamond, fields: { color: black } }"; + static String COMMON_IN_ALIAS = "{ my-name: diamond, fields: { color: black } }"; + static Shape COMMON_OUT = new Shape().name("diamond").color("black"); + static String COMMON_IN_DEFAULT = "{ fields: { color: black } }"; + static Shape COMMON_OUT_DEFAULT = new Shape().name("bob").color("black"); + static String COMMON_IN_NO_NAME = "{ fields: { color: black } }"; + static Shape COMMON_OUT_NO_NAME = new Shape().color("black"); + + @Test + public void testCommonKeyName() { + commonTopLevelFieldFixtureKeyNameAlias(). + reading( COMMON_IN_KEY_NAME, "shape" ). + writing( COMMON_OUT, "shape" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testCommonAlias() { + commonTopLevelFieldFixtureKeyNameAlias(). + read( COMMON_IN_ALIAS, "shape" ).assertResult(COMMON_OUT). + write( COMMON_OUT, "shape" ).assertResult(COMMON_IN_KEY_NAME); + } + + @Test + public void testCommonDefault() { + commonTopLevelFieldFixtureKeyNameAlias(", defaultValue: { type: string, value: bob }"). + reading( COMMON_IN_DEFAULT, "shape" ). + writing( COMMON_OUT_DEFAULT, "shape" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testEmptyDefault() { + commonTopLevelFieldFixtureKeyNameAlias(", defaultValue: { type: string, value: bob }"). + reading( "{}", "shape" ). + writing( new Shape().name("bob"), "shape" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testNameNotRequired() { + commonTopLevelFieldFixtureKeyNameAlias(). + reading( COMMON_IN_NO_NAME, "shape" ). + writing( COMMON_OUT_NO_NAME, "shape" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testNameRequired() { + try { + YomlTestFixture x = commonTopLevelFieldFixtureKeyNameAlias(", constraint: required") + .read( COMMON_IN_NO_NAME, "shape" ); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "name", "required"); + } + } + + @Test + public void testAliasConflictNiceError() { + try { + YomlTestFixture x = commonTopLevelFieldFixtureKeyNameAlias().read( + "{ my-name: name-from-alias, shape-name: name-from-key }", "shape" ); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "name-from-alias", "my-name", "name-from-key"); + } + } + + protected static YomlTestFixture extended0TopLevelFieldFixture(List extras) { + return commonTopLevelFieldFixtureKeyNameAlias(", defaultValue: { type: string, value: bob }"). + addType("shape-with-size", "{ type: \"java:"+ShapeWithSize.class.getName()+"\", interfaceTypes: [ shape ] }", + MutableList.copyOf(extras).append(topLevelFieldSerializer("{ fieldName: size, alias: shape-size }")) ); + } + + protected static YomlTestFixture extended1TopLevelFieldFixture() { + return extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name }")) ); + } + + @Test + public void testTopLevelFieldSerializersAreCollected() { + YomlTestFixture ytc = extended1TopLevelFieldFixture(); + Set serializers = MutableSet.of(); + ytc.tr.collectSerializers("shape-with-size", serializers, MutableSet.of()); + Assert.assertEquals(serializers.size(), 3, "Wrong serializers: "+serializers); + } + + String EXTENDED_IN_1 = "{ type: shape-with-size, shape-w-size-name: diamond, size: 2, fields: { color: black } }"; + Object EXTENDED_OUT_1 = new ShapeWithSize().size(2).name("diamond").color("black"); + + @Test + public void testExtendedKeyNameIsUsed() { + extended1TopLevelFieldFixture(). + reading( EXTENDED_IN_1, null ). + writing( EXTENDED_OUT_1, "shape"). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testInheritedAliasIsUsed() { + String json = "{ type: shape-with-size, my-name: diamond, size: 2, fields: { color: black } }"; + extended1TopLevelFieldFixture(). + read( json, null ).assertResult( EXTENDED_OUT_1 ). + write( EXTENDED_OUT_1, "shape-w-size" ).assertResult(EXTENDED_IN_1); + } + + String EXTENDED_IN_ORIGINAL_KEYNAME = "{ type: shape-with-size, shape-name: diamond, size: 2, fields: { color: black } }"; + + @Test + public void testOverriddenKeyNameNotUsed() { + try { + YomlTestFixture x = extended1TopLevelFieldFixture().read(EXTENDED_IN_ORIGINAL_KEYNAME, null); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "shape-name", "diamond"); + } + } + + String EXTENDED_TYPEDEF_NEW_ALIAS = "{ fieldName: name, alias: new-name }"; + + @Test + public void testInheritedKeyNameIsUsed() { + extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer(EXTENDED_TYPEDEF_NEW_ALIAS)) ) + .read(EXTENDED_IN_ORIGINAL_KEYNAME, null).assertResult(EXTENDED_OUT_1) + .write(EXTENDED_OUT_1).assertResult(EXTENDED_IN_ORIGINAL_KEYNAME); + } + + @Test + public void testOverriddenAliasIsRecognised() { + String json = "{ type: shape-with-size, new-name: diamond, size: 2, fields: { color: black } }"; + extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer(EXTENDED_TYPEDEF_NEW_ALIAS)) ) + .read( json, null ).assertResult( EXTENDED_OUT_1 ) + .write( EXTENDED_OUT_1, "shape-w-size" ).assertResult(EXTENDED_IN_ORIGINAL_KEYNAME); + } + + String EXTENDED_TYPEDEF_NEW_DEFAULT = "{ fieldName: name, defaultValue: { type: string, value: bob } }"; + Object EXTENDED_OUT_NEW_DEFAULT = new ShapeWithSize().size(2).name("bob").color("black"); + + @Test + public void testInheritedKeyNameIsUsedWithNewDefault() { + String json = "{ size: 2, fields: { color: black } }"; + extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer(EXTENDED_TYPEDEF_NEW_DEFAULT)) ) + .write(EXTENDED_OUT_NEW_DEFAULT, "shape-with-size").assertResult(json) + .read(json, "shape-with-size").assertResult(EXTENDED_OUT_NEW_DEFAULT); + } + + @Test + public void testInheritedAliasIsNotUsedIfRestricted() { + // same as testInheritedAliasIsUsed -- except fails because we say aliases-inherited: false + String json = "{ type: shape-with-size, my-name: diamond, size: 2, fields: { color: black } }"; + try { + YomlTestFixture x = extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name, aliasesInherited: false }")) ) + .read( json, null ); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "my-name", "diamond"); + } + } + + @Test + public void testFieldNameAsAlias() { + String json = "{ type: shape-with-size, name: diamond, size: 2, fields: { color: black } }"; + extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name }")) ) + .read( json, null ).assertResult( EXTENDED_OUT_1 ) + .write( EXTENDED_OUT_1 ).assertResult( EXTENDED_IN_1 ); + } + + @Test + public void testFieldNameAsAliasExcludedWhenStrict() { + String json = "{ type: shape-with-size, name: diamond, size: 2, fields: { color: black } }"; + try { + YomlTestFixture x = extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name, aliasesStrict: true }")) ) + .read( json, null ); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "name", "diamond"); + } + } + + String EXTENDED_IN_1_MANGLED = "{ type: shape-with-size, shapeWSize_Name: diamond, size: 2, fields: { color: black } }"; + + @Test + public void testFieldNameMangled() { + extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name }")) ) + .read( EXTENDED_IN_1_MANGLED, null ).assertResult( EXTENDED_OUT_1 ) + .write( EXTENDED_OUT_1 ).assertResult( EXTENDED_IN_1 ); + } + + @Test + public void testFieldNameManglesExcludedWhenStrict() { + try { + YomlTestFixture x = extended0TopLevelFieldFixture( MutableList.of( + topLevelFieldSerializer("{ fieldName: name, keyName: shape-w-size-name, aliasesStrict: true }")) ) + .read( EXTENDED_IN_1_MANGLED, null ); + Asserts.shouldHaveFailedPreviously("Returned "+x.lastReadResult+" when should have thrown"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "shapeWSize_Name", "diamond"); + } + } + + static String SIMPLE_IN_ALL_FIELDS_TOP_LEVEL = "{ color: black, name: diamond }"; + @Test public void testAllFieldsTopLevel() { + YomlTestFixture y = YomlTestFixture.newInstance(). + addType("shape", Shape.class, MutableList.of(new AllFieldsTopLevel())); + + y.read( SIMPLE_IN_ALL_FIELDS_TOP_LEVEL, "shape" ).assertResult( SIMPLE_OUT ). + write( SIMPLE_OUT, "shape" ).assertResult( SIMPLE_IN_ALL_FIELDS_TOP_LEVEL ); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlAnnotationTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlAnnotationTests.java new file mode 100644 index 0000000000..ad3d179fb3 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlAnnotationTests.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import org.apache.brooklyn.util.yoml.annotations.Alias; +import org.apache.brooklyn.util.yoml.annotations.YomlAllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.annotations.YomlTopLevelField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.base.Objects; + +/** Tests that the default serializers can read/write types and fields. + *

+ * And shows how to use them at a low level. + */ +public class YomlAnnotationTests { + + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(YomlAnnotationTests.class); + + static class TopLevelFieldsExample { + @Alias("shape") + static class Shape { + @YomlTopLevelField + String name; + @YomlTopLevelField + @Alias(value={"kolor","couleur"}, preferred="colour") + String color; + + @Override + public boolean equals(Object xo) { + if (xo==null || !Objects.equal(getClass(), xo.getClass())) return false; + Shape x = (Shape) xo; + return Objects.equal(name, x.name) && Objects.equal(color, x.color); + } + @Override + public String toString() { + return Objects.toStringHelper(this).add("name", name).add("color", color).omitNullValues().toString(); + } + @Override + public int hashCode() { return Objects.hashCode(name, color); } + + public Shape name(String name) { this.name = name; return this; } + public Shape color(String color) { this.color = color; return this; } + } + } + + @Test + public void testYomlFieldsAtTopLevel() { + TopLevelFieldsExample.Shape shape = new TopLevelFieldsExample.Shape().name("nifty_shape").color("blue"); + YomlTestFixture.newInstance(). + addTypeWithAnnotations(TopLevelFieldsExample.Shape.class). + read("{ name: nifty_shape, couleur: blue }", "shape").assertResult(shape). + write(shape).assertResult("{ type: shape, colour: blue, name: nifty_shape }"); + } + + static class AllFieldsTopLevelExample { + @YomlAllFieldsTopLevel + @Alias("shape") + static class Shape { + String name; + @YomlTopLevelField + @Alias(value={"kolor","couleur"}, preferred="colour") + String color; + + @Override + public boolean equals(Object xo) { + if (xo==null || !Objects.equal(getClass(), xo.getClass())) return false; + Shape x = (Shape) xo; + return Objects.equal(name, x.name) && Objects.equal(color, x.color); + } + @Override + public String toString() { + return Objects.toStringHelper(this).add("name", name).add("color", color).omitNullValues().toString(); + } + @Override + public int hashCode() { return Objects.hashCode(name, color); } + + public Shape name(String name) { this.name = name; return this; } + public Shape color(String color) { this.color = color; return this; } + } + } + + @Test + public void testYomlAllFields() { + AllFieldsTopLevelExample.Shape shape = new AllFieldsTopLevelExample.Shape().name("nifty_shape").color("blue"); + YomlTestFixture.newInstance(). + addTypeWithAnnotations(AllFieldsTopLevelExample.Shape.class). + read("{ name: nifty_shape, couleur: blue }", "shape").assertResult(shape). + write(shape).assertResult("{ type: shape, colour: blue, name: nifty_shape }"); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlBasicTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlBasicTests.java new file mode 100644 index 0000000000..1aaae2d896 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlBasicTests.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.math.RoundingMode; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.yoml.Yoml; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.base.Objects; + +/** Tests that the default serializers can read/write types and fields. + *

+ * And shows how to use them at a low level. + */ +public class YomlBasicTests { + + private static final Logger log = LoggerFactory.getLogger(YomlBasicTests.class); + + static class Shape { + String name; + String color; + + @Override + public boolean equals(Object xo) { + if (xo==null || !Objects.equal(getClass(), xo.getClass())) return false; + Shape x = (Shape) xo; + return Objects.equal(name, x.name) && Objects.equal(color, x.color); + } + @Override + public String toString() { + return Objects.toStringHelper(this).add("name", name).add("color", color).omitNullValues().toString(); + } + @Override + public int hashCode() { return Objects.hashCode(name, color); } + + public Shape name(String name) { this.name = name; return this; } + public Shape color(String color) { this.color = color; return this; } + } + + // very basic tests illustrating read/write + + @Test + public void testReadJavaType() { + MockYomlTypeRegistry tr = new MockYomlTypeRegistry(); + tr.put("shape", Shape.class); + Yoml y = Yoml.newInstance(tr); + Object resultO = y.read("{ type: shape }", "object"); + + Assert.assertNotNull(resultO); + Assert.assertTrue(resultO instanceof Shape, "Wrong result type: "+resultO); + Shape result = (Shape)resultO; + Assert.assertNull(result.name); + Assert.assertNull(result.color); + } + + @Test + public void testReadFieldInFields() { + MockYomlTypeRegistry tr = new MockYomlTypeRegistry(); + tr.put("shape", Shape.class); + Yoml y = Yoml.newInstance(tr); + Object resultO = y.read("{ type: shape, fields: { color: red } }", "object"); + + Assert.assertNotNull(resultO); + Assert.assertTrue(resultO instanceof Shape, "Wrong result type: "+resultO); + Shape result = (Shape)resultO; + Assert.assertNull(result.name); + Assert.assertEquals(result.color, "red"); + } + + @Test + public void testWritePrimitiveFieldInFields() { + MockYomlTypeRegistry tr = new MockYomlTypeRegistry(); + Yoml y = Yoml.newInstance(tr); + + Shape s = new ShapeWithSize(); + s.color = "red"; + Object resultO = y.write(s); + + Assert.assertNotNull(resultO); + String out = Jsonya.newInstance().add(resultO).toString(); + String expected = Jsonya.newInstance().add("type", "java"+":"+ShapeWithSize.class.getName()) + .at("fields").add("color", "red", "size", 0).root().toString(); + Assert.assertEquals(out, expected); + } + + // now using the fixture + + @Test + public void testFieldInFieldsUsingTestFixture() { + YomlTestFixture.newInstance().writing( new Shape().color("red") ). + reading( + Jsonya.newInstance().add("type", "java"+":"+Shape.class.getName()) + .at("fields").add("color", "red").root().toString() ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testStringPrimitiveWhereTypeKnown() { + YomlTestFixture.newInstance(). + write("hello", "string").assertResult("hello"). + read("hello", "string").assertResult("hello"); + } + + @Test + public void testStringPrimitiveWhereTypeUnknown() { + YomlTestFixture.newInstance(). + write("hello").assertResult("{ type: string, value: hello }"). + read("{ type: string, value: hello }", null).assertResult("hello"); + } + @Test + public void testIntPrimitiveWhereTypeUnknown() { + YomlTestFixture.newInstance(). + write(42).assertResult("{ type: int, value: 42 }"). + read("{ type: int, value: 42 }", null).assertResult(42); + } + @Test + public void testEnumWhereTypeKnown() { + YomlTestFixture.newInstance(). + addType("rounding-mode", RoundingMode.class). + write(RoundingMode.HALF_EVEN, "rounding-mode").assertResult(RoundingMode.HALF_EVEN.name()). + read(RoundingMode.HALF_EVEN.name(), "rounding-mode").assertResult(RoundingMode.HALF_EVEN). + read("half-even", "rounding-mode").assertResult(RoundingMode.HALF_EVEN); + } + + @Test + public void testEnumWhereTypeUnknown() { + String json = "{ type: rounding-mode, value: "+RoundingMode.HALF_EVEN.name()+" }"; + YomlTestFixture.newInstance(). + addType("rounding-mode", RoundingMode.class). + write(RoundingMode.HALF_EVEN).assertResult(json). + read(json, null).assertResult(RoundingMode.HALF_EVEN); + } + + @Test + public void testRegisteredType() { + YomlTestFixture.newInstance(). + addType("shape", Shape.class). + writing(new Shape()). + reading( "{ type: shape }" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testExpectedType() { + YomlTestFixture.newInstance(). + addType("shape", Shape.class). + writing(new Shape().color("red"), "shape"). + reading( "{ fields: { color: red } }", "shape" ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testExtraFieldError() { + try { + YomlTestFixture.newInstance(). + addType("shape", Shape.class). + read( "{ type: shape, fields: { size: 4 } }", "shape" ); + Asserts.shouldHaveFailedPreviously("should complain about fields still existing"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "size"); + } + } + + static class ShapeWithSize extends Shape { + int size; + + @Override + public boolean equals(Object xo) { + return super.equals(xo) && Objects.equal(size, ((ShapeWithSize)xo).size); + } + + public ShapeWithSize size(int size) { this.size = size; return this; } + public ShapeWithSize name(String name) { return (ShapeWithSize)super.name(name); } + public ShapeWithSize color(String color) { return (ShapeWithSize)super.color(color); } + + @Override + public String toString() { + return Objects.toStringHelper(this).add("name", name).add("color", color).add("size", size).omitNullValues().toString(); + } + } + + @Test + public void testFieldInExtendedClassInFields() { + YomlTestFixture.newInstance().writing( new ShapeWithSize().size(4).color("red") ). + reading( + Jsonya.newInstance().add("type", "java"+":"+ShapeWithSize.class.getName()) + .at("fields").add("color", "red", "size", 4).root().toString() ). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testFieldInExtendedClassInFieldsDefault() { + // note we get 0 written + YomlTestFixture.newInstance().writing( new ShapeWithSize().color("red") ). + reading( + Jsonya.newInstance().add("type", "java"+":"+ShapeWithSize.class.getName()) + .at("fields").add("color", "red", "size", 0).root().toString() ). + doReadWriteAssertingJsonMatch(); + } + + + @Test + public void testFailOnUnknownType() { + try { + YomlTestFixture ytc = YomlTestFixture.newInstance().read("{ type: shape }", null); + Asserts.shouldHaveFailedPreviously("Got "+ytc.lastReadResult+" when we should have failed due to unknown type shape"); + } catch (Exception e) { + try { + Asserts.expectedFailureContainsIgnoreCase(e, "no ", "type", "shape", "available"); + } catch (Throwable e2) { + log.warn("Failure detail: "+e, e); + throw Exceptions.propagate(e2); + } + } + } + + static class ShapePair { + String pairName; + Shape shape1; + Shape shape2; + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ShapePair)) return false; + ShapePair sp = (ShapePair)obj; + return Objects.equal(pairName, sp.pairName) && Objects.equal(shape1, sp.shape1) && Objects.equal(shape2, sp.shape2); + } + } + + @Test + public void testWriteComplexFieldInFields() { + ShapePair pair = new ShapePair(); + pair.pairName = "red and blue"; + pair.shape1 = new Shape().color("red"); + pair.shape2 = new ShapeWithSize().size(8).color("blue"); + + YomlTestFixture.newInstance(). + addType("shape", Shape.class). + addType("shape-with-size", ShapeWithSize.class). + addType("shape-pair", ShapePair.class). + writing(pair). + reading( + Jsonya.newInstance().add("type", "shape-pair") + .at("fields").add("pairName", pair.pairName) + .at("shape1") + // .add("type", "shape") // default is suppressed + .at("fields").add("color", pair.shape1.color) + .root() + .at("fields", "shape2") + .add("type", "shape-with-size") + .at("fields").add("color", pair.shape2.color, "size", ((ShapeWithSize)pair.shape2).size) + .root().toString() + ).doReadWriteAssertingJsonMatch(); + } + + static class ShapeWithWeirdFields extends ShapeWithSize { + static int aStatic = 1; + transient int aTransient = 11; + + /** masks name in parent */ + String name; + ShapeWithWeirdFields realName(String name) { this.name = name; return this; } + } + + @Test + public void testStaticNotWrittenButExtendedItemsAre() { + ShapeWithWeirdFields shape = new ShapeWithWeirdFields(); + shape.size(4).name("weird-shape"); + shape.realName("normal-trust-me"); + ShapeWithWeirdFields.aStatic = 2; + shape.aTransient = 12; + + YomlTestFixture.newInstance(). + addType("shape-weird", ShapeWithWeirdFields.class). + writing(shape).reading("{ type: shape-weird, " + + "fields: { name: normal-trust-me, " + + Shape.class.getCanonicalName()+"."+"name: weird-shape, " + + "size: 4 " + + "} }"). + doReadWriteAssertingJsonMatch(); + } + + @Test + public void testStaticNotRead() { + ShapeWithWeirdFields.aStatic = 3; + try { + YomlTestFixture.newInstance(). + addType("shape-weird", ShapeWithWeirdFields.class). + read("{ type: shape-weird, " + + "fields: { aStatic: 4 " + "} }", null); + Assert.assertEquals(3, ShapeWithWeirdFields.aStatic); + Asserts.shouldHaveFailedPreviously(); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "aStatic"); + } + Assert.assertEquals(3, ShapeWithWeirdFields.aStatic); + } + + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlConfigKeyGenericsTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlConfigKeyGenericsTests.java new file mode 100644 index 0000000000..c1fdbc9e18 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlConfigKeyGenericsTests.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Map; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.yoml.annotations.YomlConfigMapConstructor; +import org.apache.brooklyn.util.yoml.tests.TopLevelConfigKeysTests.MockConfigKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.reflect.TypeToken; + +/** Tests that the default serializers can read/write types and fields. + *

+ * And shows how to use them at a low level. + */ +public class YomlConfigKeyGenericsTests { + + private static final Logger log = LoggerFactory.getLogger(YomlConfigKeyGenericsTests.class); + + @YomlConfigMapConstructor("conf") + static class M0 { + Map conf = MutableMap.of(); + M0(Map keys) { this.conf.putAll(keys); } + + @Override + public boolean equals(Object obj) { + return (obj instanceof M0) && ((M0)obj).conf.equals(conf); + } + @Override + public int hashCode() { + return conf.hashCode(); + } + @Override + public String toString() { + return super.toString()+conf; + } + } + + @SuppressWarnings({ "rawtypes" }) + static class MG extends M0 { + static MockConfigKey KR = new MockConfigKey(Map.class, "kr"); + @SuppressWarnings("serial") + static MockConfigKey> KG = new MockConfigKey>(new TypeToken>() {}, "kg"); + + MG(Map keys) { super(keys); } + } + + static final Map CONF_MAP = MutableMap.of("x", 1); + final static String RAW_OUT = "{ type: mg, kr: { type: 'map', value: { x: 1 } } }"; + static final MG RAW_IN = new MG(MutableMap.of("kr", CONF_MAP)); + + @Test + public void testWriteRaw() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("mg", MG.class); + + y.write(RAW_IN); + log.info("M1B written as: "+y.lastWriteResult); + YomlTestFixture.assertEqualish(Jsonya.newInstance().add(y.lastWriteResult).toString(), + RAW_OUT, "wrong serialization"); + } + + @Test + public void testReadRaw() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("mg", MG.class); + + y.read(RAW_OUT, null); + + MG mg = (MG)y.lastReadResult; + + Asserts.assertEquals(mg.conf.get(MG.KR.getName()), CONF_MAP); + Asserts.assertEquals(mg, RAW_IN); + } + + final static String GEN_OUT = "{ type: mg, kg: { x: 1 } }"; + static final MG GEN_IN = new MG(MutableMap.of("kg", CONF_MAP)); + + @Test + public void testWriteGen() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("mg", MG.class); + + y.write(GEN_IN); + log.info("M1B written as: "+y.lastWriteResult); + YomlTestFixture.assertEqualish(Jsonya.newInstance().add(y.lastWriteResult).toString(), + GEN_OUT, "wrong serialization"); + } + + @Test + public void testReadGen() { + YomlTestFixture y = YomlTestFixture.newInstance().addTypeWithAnnotations("mg", MG.class); + + y.read(GEN_OUT, null); + + MG mg = (MG)y.lastReadResult; + + Asserts.assertEquals(mg.conf.get(MG.KG.getName()), CONF_MAP); + Asserts.assertEquals(mg, GEN_IN); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlMapListTests.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlMapListTests.java new file mode 100644 index 0000000000..ddb3c583d8 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlMapListTests.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.collections.MutableSet; +import org.apache.brooklyn.util.yoml.serializers.AllFieldsTopLevel; +import org.apache.brooklyn.util.yoml.tests.YomlBasicTests.Shape; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** Tests basic map/list parsing */ +public class YomlMapListTests { + + YomlTestFixture y = YomlTestFixture.newInstance(); + + String MAP1_JSON = "{ a: 1, b: bbb }"; + Map MAP1_OBJ = MutableMap.of("a", 1, "b", "bbb"); + + @Test public void testReadMap() { y.read(MAP1_JSON, "json").assertResult(MAP1_OBJ); } + @Test public void testWriteMap() { y.write(MAP1_OBJ, "json").assertResult(MAP1_JSON); } + + String MAP1_JSON_EXPLICIT_TYPE = "{ type: \"map\", value: "+MAP1_JSON+" }"; + @Test public void testReadMapNoTypeExpected() { y.read(MAP1_JSON_EXPLICIT_TYPE, null).assertResult(MAP1_OBJ); } + @Test public void testWriteMapNoTypeExpected() { y.write(MAP1_OBJ, null).assertResult(MAP1_JSON_EXPLICIT_TYPE); } + + String MAP1_JSON_OBJECT_OBJECT = "[ { key: { type: string, value: a }, value: { type: int, value: 1 } }, "+ + "{ key: { type: string, value: b }, value: { type: string, value: bbb } } ]"; + @Test public void testReadMapVerbose() { y.read(MAP1_JSON_OBJECT_OBJECT, "map").assertResult(MAP1_OBJ); } + + // a generic type Object is written in long form even if it's a primitive, to guard against the wrong interpretation of the primitive + String MAP1_JSON_STRING_OBJECT = "{ a: { type: int, value: 1 }, b: { type: string, value: bbb } }"; + @Test public void testReadMapVerboseStringKey() { y.read(MAP1_JSON_STRING_OBJECT, "map").assertResult(MAP1_OBJ); } + @Test public void testReadMapVerboseStringLongTypeFormatKey() { y.read(MAP1_JSON_STRING_OBJECT, "java:java.util.Map").assertResult(MAP1_OBJ); } + @Test public void testReadMapVerboseJsonKey() { y.read(MAP1_JSON_STRING_OBJECT, "map").assertResult(MAP1_OBJ); } + + String LIST1_JSON = "[ a, 1, b ]"; + List LIST1_OBJ = MutableList.of("a", 1, "b"); + + @Test public void testReadList() { y.read(LIST1_JSON, "json").assertResult(LIST1_OBJ); } + @Test public void testWriteList() { y.write(LIST1_OBJ, "json").assertResult(LIST1_JSON); } + @Test public void testReadListListJson() { y.read(LIST1_JSON, "list").assertResult(LIST1_OBJ); } + @Test public void testWriteListListJson() { y.write(LIST1_OBJ, "list").assertResult(LIST1_JSON); } + + @Test public void testReadListNoTypeAnywhereIsError() { + try { + y.read(LIST1_JSON, null); + Asserts.shouldHaveFailedPreviously("Got "+y.lastResult+" when should have failed"); + } catch (Exception e) { Asserts.expectedFailureContainsIgnoreCase(e, "'a'", "unknown type"); } + } + + String LIST1_JSON_LIST_TYPE = "{ type: json, value: "+LIST1_JSON+" }"; + String LIST1_JSON_LIST_JSON_TYPE = "{ type: list, value: "+LIST1_JSON+" }"; + @Test public void testReadListNoTypeExpected() { y.read(LIST1_JSON_LIST_TYPE, null).assertResult(LIST1_OBJ); } + @Test public void testReadListExplicitTypeNoTypeExpected() { y.read(LIST1_JSON_LIST_TYPE, null).assertResult(LIST1_OBJ); } + @Test public void testWriteListNoTypeExpected() { y.write(LIST1_OBJ, null).assertResult(LIST1_JSON_LIST_JSON_TYPE); } + // write prefers type: json syntax above if appropriate; otherwise will to LIST_OBJECT syntax below + + String LIST1_JSON_OBJECT = "[ { type: string, value: a }, { type: int, value: 1 }, { type: string, value: b } ]"; + @Test public void testReadListObject() { y.read(LIST1_JSON_OBJECT, "list").assertResult(LIST1_OBJ); } + @Test public void testWriteListObject() { y.write(LIST1_OBJ, "list").assertResult(LIST1_JSON_OBJECT); } + @Test public void testReadListRawType() { y.read(LIST1_JSON_OBJECT, "list").assertResult(LIST1_OBJ); } + @Test public void testWriteListRawType() { y.write(LIST1_OBJ, "list").assertResult(LIST1_JSON_LIST_JSON_TYPE); } + @Test public void testReadListJsonType() { y.read(LIST1_JSON_OBJECT, "list").assertResult(LIST1_OBJ); } + @Test public void testReadListJsonTypeInBody() { y.read(LIST1_JSON_LIST_JSON_TYPE, null).assertResult(LIST1_OBJ); } + @Test public void testReadListJsonTypeDeclaredAndInBody() { y.read(LIST1_JSON_LIST_JSON_TYPE, "list").assertResult(LIST1_OBJ); } + + @Test public void testArraysAsList() { + y.reading("[ a, b ]", "list").writing(Arrays.asList("a", "b"), "list") + .doReadWriteAssertingJsonMatch(); + } + + Set SET1_OBJ = MutableSet.of("a", 1, "b"); + String SET1_JSON = "{ type: set, value: [ a, 1, b ] }"; + @Test public void testWriteSet() { y.write(SET1_OBJ, "set").assertResult(LIST1_JSON); } + @Test public void testWriteSetNoType() { y.write(SET1_OBJ, null).assertResult(SET1_JSON); } + @Test public void testReadSet() { y.read(SET1_JSON, "set").assertResult(SET1_OBJ); } + @Test public void testWriteSetJson() { y.write(SET1_OBJ, "json").assertResult(LIST1_JSON); } + + @Test public void testReadWithShape() { + y.tr.put("shape", Shape.class); + Shape shape = new Shape().name("my-shape"); + + Map m1 = MutableMap.of("k1", shape); + String MAP_W_SHAPE = "{ k1: { type: shape, fields: { name: my-shape } } }"; + y.read(MAP_W_SHAPE, "map").assertResult(m1); + y.write(m1, "map").assertResult(MAP_W_SHAPE); + y.write(m1, null).assertResult("{ type: map, value: " + MAP_W_SHAPE + " }"); + + Map m2 = MutableMap.of(shape, "v1", "k2", 2); + Map m3 = MutableMap.of(shape, "v1", "k2", 2); + Assert.assertEquals(m2, m3); + String MAP_W_SHAPE_KEY_JSON = "[ { key: { type: shape, fields: { name: my-shape } }, value: v1 }, { k2: 2 } ]"; + String MAP_W_SHAPE_KEY_NON_GENERIC = "[ { key: { type: shape, fields: { name: my-shape } }, value: { type: string, value: v1 } }, { k2: { type: int, value: 2 } } ]"; + y.read(MAP_W_SHAPE_KEY_JSON, "map").assertResult(m2); + y.write(m2, "map").assertResult(MAP_W_SHAPE_KEY_JSON); + y.write(m2, "map").assertResult(MAP_W_SHAPE_KEY_NON_GENERIC); + } + + @Test public void testReadWithEnum() { + y.tr.put("rounding-mode", RoundingMode.class); + + Map m1 = MutableMap.of("k1", RoundingMode.UP); + String MAP_W_RM = "{ k1: { type: rounding-mode, value: UP } }"; + y.read(MAP_W_RM, "map").assertResult(m1); + y.write(m1, "map").assertResult(MAP_W_RM); + y.write(m1, null).assertResult("{ type: map, value: " + MAP_W_RM + " }"); + + String MAP_W_RM_TYPE_KNOWN = "{ k1: UP }"; + y.read(MAP_W_RM_TYPE_KNOWN, "map").assertResult(m1); + y.write(m1, "map").assertResult(MAP_W_RM_TYPE_KNOWN); + } + + static class TestingGenericsOnFields { + List list; + Map map; + Set set; + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TestingGenericsOnFields)) return false; + TestingGenericsOnFields gf = (TestingGenericsOnFields)obj; + return Objects.equals(list, gf.list) && Objects.equals(map, gf.map) && Objects.equals(set, gf.set); + } + @Override + public int hashCode() { + return Objects.hash(list, map, set); + } + @Override + public String toString() { + return com.google.common.base.Objects.toStringHelper(this).add("list", list).add("map", map).add("set", set).omitNullValues().toString(); + } + } + + @Test public void testGenericList() { + y.tr.put("gf", TestingGenericsOnFields.class, MutableList.of(new AllFieldsTopLevel())); + TestingGenericsOnFields gf; + + gf = new TestingGenericsOnFields(); + gf.list = MutableList.of(RoundingMode.UP); + y.reading("{ list: [ UP ] }", "gf").writing(gf, "gf").doReadWriteAssertingJsonMatch(); + } + @Test public void testGenericMap() { + y.tr.put("gf", TestingGenericsOnFields.class, MutableList.of(new AllFieldsTopLevel())); + TestingGenericsOnFields gf; + String json; + gf = new TestingGenericsOnFields(); + gf.map = MutableMap.of(RoundingMode.UP, RoundingMode.DOWN); + json = "{ map: [ { key: UP, value: DOWN } ] }"; + y.reading(json, "gf").writing(gf, "gf").doReadWriteAssertingJsonMatch(); + // TODO make it smart enough to realize all keys are strings and adjust, so (a) this works, and (b) we can swap json2 with json above + // (but enum keys are not a high priority) +// String json2 = "{ map: { UP: DOWN } }"; +// y.read(json2, "gf").assertResult(gf); + } + + @Test public void testGenericListSet() { + y.tr.put("gf", TestingGenericsOnFields.class, MutableList.of(new AllFieldsTopLevel())); + TestingGenericsOnFields gf; + String json; + gf = new TestingGenericsOnFields(); + gf.set = MutableSet.of(RoundingMode.UP); + json = "{ set: [ UP ] }"; + String json2 = "{ set: { type: set, value: [ UP ] } }"; + y.read(json2, "gf").assertResult(gf); + y.reading(json, "gf").writing(gf, "gf").doReadWriteAssertingJsonMatch(); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlTestFixture.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlTestFixture.java new file mode 100644 index 0000000000..ad4defc423 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlTestFixture.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yoml.Yoml; +import org.apache.brooklyn.util.yoml.YomlConfig; +import org.apache.brooklyn.util.yoml.YomlSerializer; +import org.apache.brooklyn.util.yoml.annotations.YomlAnnotations; +import org.apache.brooklyn.util.yoml.serializers.InstantiateTypeFromRegistryUsingConfigMap; +import org.testng.Assert; + +public class YomlTestFixture { + + public static YomlTestFixture newInstance() { return new YomlTestFixture(); } + public static YomlTestFixture newInstance(YomlConfig config) { return new YomlTestFixture(config); } + + final MockYomlTypeRegistry tr; + final Yoml y; + + public YomlTestFixture() { + this(YomlConfig.Builder.builder().serializersPostAddDefaults().build()); + } + public YomlTestFixture(YomlConfig config) { + if (config.getTypeRegistry()==null) { + tr = new MockYomlTypeRegistry(); + config = YomlConfig.Builder.builder(config).typeRegistry(tr).build(); + } else { + tr = null; + } + y = Yoml.newInstance(config); + } + + Object writeObject; + String writeObjectExpectedType; + String lastWriteExpectedType; + Object lastWriteResult; + String readObject; + String readObjectExpectedType; + Object lastReadResult; + String lastReadExpectedType; + Object lastResult; + + public YomlTestFixture writing(Object objectToWrite) { + return writing(objectToWrite, null); + } + public YomlTestFixture writing(Object objectToWrite, String expectedType) { + writeObject = objectToWrite; + writeObjectExpectedType = expectedType; + return this; + } + public YomlTestFixture reading(String stringToRead) { + return reading(stringToRead, null); + } + public YomlTestFixture reading(String stringToRead, String expectedType) { + readObject = stringToRead; + readObjectExpectedType = expectedType; + return this; + } + + public YomlTestFixture write(Object objectToWrite) { + return write(objectToWrite, null); + } + public YomlTestFixture write(Object objectToWrite, String expectedType) { + writing(objectToWrite, expectedType); + lastWriteExpectedType = expectedType; + lastWriteResult = y.write(objectToWrite, expectedType); + lastResult = lastWriteResult; + return this; + } + public YomlTestFixture read(String objectToRead, String expectedType) { + reading(objectToRead, expectedType); + lastReadExpectedType = expectedType; + lastReadResult = y.read(objectToRead, expectedType); + lastResult = lastReadResult; + return this; + } + public YomlTestFixture writeLastRead() { + write(lastReadResult, lastReadExpectedType); + return this; + } + public YomlTestFixture readLastWrite() { + read(asJson(lastWriteResult), lastWriteExpectedType); + return this; + } + + public YomlTestFixture assertResult(Object expectation) { + if (expectation instanceof String) { + if (lastResult instanceof Map || lastResult instanceof Collection) { + assertEqualish(asJson(lastResult), expectation, "Result as JSON string does not match expectation"); + } else { + assertEqualish(Strings.toString(lastResult), expectation, "Result toString does not match expectation"); + } + } else { + Assert.assertEquals(lastResult, expectation); + } + return this; + } + + static String asJson(Object o) { + return Jsonya.newInstance().add(o).toString(); + } + + public YomlTestFixture doReadWriteAssertingJsonMatch() { + read(readObject, readObjectExpectedType); + write(writeObject, writeObjectExpectedType); + return assertLastsMatch(); + } + + public YomlTestFixture assertLastsMatch() { + assertEqualish(asJson(lastWriteResult), readObject, "Write output should match read input"); + assertEqualish(lastReadResult, writeObject, "Read output should match write input"); + return this; + } + + private static String removeGuff(String input) { + return Strings.replaceAll(input, MutableMap.of("\"", "", "\'", "") + .add("=", ": ").add(": ", ": ").add(" :", ":") + .add(" ,", ",").add(", ", ",") + .add("{ ", "{").add(" {", "{") + .add(" }", "}").add("} ", "}") + ); + } + + static void assertEqualish(Object s1, Object s2, String message) { + if (s1 instanceof String) s1 = removeGuff((String)s1); + if (s2 instanceof String) s2 = removeGuff((String)s2); + Assert.assertEquals(s1, s2, message); + } + + public void assertLastWriteIgnoringQuotes(String expected, String message) { + assertEqualish(Jsonya.newInstance().add(getLastWriteResult()).toString(), expected, message); + } + public void assertLastWriteIgnoringQuotes(String expected) { + assertEqualish(Jsonya.newInstance().add(getLastWriteResult()).toString(), expected, "mismatch on last write"); + } + + // methods below require using the default registry, will NPE otherwise + + public YomlTestFixture addType(String name, Class type) { tr.put(name, type); return this; } + public YomlTestFixture addType(String name, Class type, List serializers) { tr.put(name, type, serializers); return this; } + public YomlTestFixture addType(String name, String yamlDefinition) { tr.put(name, yamlDefinition); return this; } + public YomlTestFixture addType(String name, String yamlDefinition, List serializers) { tr.put(name, yamlDefinition, serializers); return this; } + + public YomlTestFixture addTypeWithAnnotations(Class type) { + return addTypeWithAnnotations(null, type); + } + public YomlTestFixture addTypeWithAnnotations(String optionalName, Class type) { + Set serializers = annotationsProvider().findSerializerAnnotations(type, false); + for (String n: new YomlAnnotations().findTypeNamesFromAnnotations(type, optionalName, false)) { + tr.put(n, type, serializers); + } + return this; + } + public YomlTestFixture addTypeWithAnnotationsAndConfigFieldsIgnoringInheritance(String optionalName, Class type, + Map configFieldsToKeys) { + Set serializers = annotationsProvider().findSerializerAnnotations(type, false); + for (Map.Entry entry: configFieldsToKeys.entrySet()) { + serializers.addAll( InstantiateTypeFromRegistryUsingConfigMap.newFactoryIgnoringInheritance().newConfigKeyClassScanningSerializers( + entry.getKey(), entry.getValue(), true) ); + } + for (String n: new YomlAnnotations().findTypeNamesFromAnnotations(type, optionalName, false)) { + tr.put(n, type, serializers); + } + return this; + } + protected YomlAnnotations annotationsProvider() { + return new YomlAnnotations(); + } + + public Object getLastReadResult() { + return lastReadResult; + } + public Object getLastWriteResult() { + return lastWriteResult; + } +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlUtilsTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlUtilsTest.java new file mode 100644 index 0000000000..ec8da9e53d --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/yoml/tests/YomlUtilsTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.yoml.tests; + +import java.util.List; + +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.yoml.internal.YomlUtils; +import org.apache.brooklyn.util.yoml.internal.YomlUtils.GenericsParse; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.google.common.reflect.TypeToken; + +public class YomlUtilsTest { + + @Test + public void testGenericsParse() { + GenericsParse gp = new YomlUtils.GenericsParse("foo"); + Assert.assertEquals(gp.baseType, "foo"); + Assert.assertEquals(gp.subTypes, MutableList.of("bar")); + Assert.assertTrue(gp.isGeneric); + } + + @SuppressWarnings("serial") + @Test + public void testGenericsWrite() { + MockYomlTypeRegistry tr = new MockYomlTypeRegistry(); + tr.put("lst", List.class); + tr.put("str", String.class); + Assert.assertEquals(YomlUtils.getTypeNameWithGenerics(new TypeToken>() {}, tr), + "lst"); + } + +}