diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java index 86f1c825e6..4e3437dd0f 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslDeferredSupplier.java @@ -37,7 +37,6 @@ import org.apache.brooklyn.util.core.task.ImmediateSupplier; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; -import org.apache.brooklyn.util.guava.Maybe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 f43d33c4b9..16d5c5a389 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 @@ -32,13 +32,12 @@ import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode; import org.apache.brooklyn.camp.spi.resolve.interpret.PlanInterpretationNode.Role; 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.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; - /** * {@link PlanInterpreter} which understands the $brooklyn DSL */ @@ -157,15 +156,6 @@ public Object evaluateOn(Object o, FunctionWithArgs f, boolean deepEvaluation) { if (f.getArgs()==null) throw new IllegalStateException("Invalid function-only expression '"+f.getFunction()+"'"); - Class clazz; - if (o instanceof Class) { - clazz = (Class)o; - } else { - clazz = o.getClass(); - } - if (!(clazz.getPackage().getName().startsWith(BrooklynDslCommon.class.getPackage().getName()))) - throw new IllegalArgumentException("Not permitted to invoke function on '"+clazz+"' (outside allowed package scope)"); - String fn = f.getFunction(); fn = Strings.removeFromStart(fn, "$brooklyn:"); if (fn.startsWith("function.")) { @@ -175,19 +165,22 @@ public Object evaluateOn(Object o, FunctionWithArgs f, boolean deepEvaluation) { o = BrooklynDslCommon.Functions.class; fn = Strings.removeFromStart(fn, "function."); } + List args = new ArrayList<>(); + for (Object arg: f.getArgs()) { + args.add( deepEvaluation ? evaluate(arg, true) : arg ); + } try { - List args = new ArrayList<>(); - for (Object arg: f.getArgs()) { - args.add( deepEvaluation ? evaluate(arg, true) : arg ); + if (o instanceof BrooklynDslDeferredSupplier && !(o instanceof DslCallable)) { + return new DslDeferredFunctionCall((BrooklynDslDeferredSupplier) o, fn, args); + } else { + // Would prefer to keep the invocation logic encapsulated in DslDeferredFunctionCall, but + // for backwards compatibility will evaluate as much as possible eagerly (though it shouldn't matter in theory). + return DslDeferredFunctionCall.invokeOn(o, fn, args); } - Optional v = Reflections.invokeMethodWithArgs(o, fn, args); - if (v.isPresent()) return v.get(); } catch (Exception e) { Exceptions.propagateIfFatal(e); - throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fn+"' on '"+o+"'")); + throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fn+"' on '"+o+"' with arguments "+args+"")); } - - throw new IllegalArgumentException("No such function '"+fn+"' on "+o); } - + } diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslCallable.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslCallable.java new file mode 100644 index 0000000000..4eeddb747e --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslCallable.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed 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.brooklyn.spi.dsl; + +import org.apache.brooklyn.util.core.task.DeferredSupplier; + +/** + * Marker interface so the evaluator can tell apart objects which are {@link DeferredSupplier} + * but which expect DSL methods called on them instead of the value they supply. + */ +public interface DslCallable { + +} diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java new file mode 100644 index 0000000000..ad8c4b154c --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java @@ -0,0 +1,191 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed 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.brooklyn.spi.dsl; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.Callable; + +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon; +import org.apache.brooklyn.core.entity.EntityInternal; +import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; +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.javalang.Reflections; + +import com.google.common.base.Joiner; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; + +public class DslDeferredFunctionCall extends BrooklynDslDeferredSupplier { + + private static final long serialVersionUID = 3243262633795112155L; + + // TODO should this be some of the super types? + private BrooklynDslDeferredSupplier object; + private String fnName; + private List args; + + public DslDeferredFunctionCall(BrooklynDslDeferredSupplier o, String fn, List args) { + this.object = o; + this.fnName = fn; + this.args = args; + } + + @Override + public Maybe getImmediately() { + Maybe obj = resolveImmediate(object); + if (obj.isPresent()) { + if (obj.isNull()) { + throw new IllegalArgumentException("Deferred function call, " + object + + " evaluates to null (when calling " + fnName + "(" + toString(args) + "))"); + } + return Maybe.of(invokeOn(obj.get())); + } + return Maybe.absent("Could not evaluate immediately " + object); + } + + @Override + public Task newTask() { + return Tasks.builder() + .displayName("Deferred function call " + object + "." + fnName + "(" + toString(args) + ")") + .tag(BrooklynTaskTags.TRANSIENT_TASK_TAG) + .dynamic(false) + .body(new Callable() { + @Override + public Object call() throws Exception { + Object obj = DslDeferredFunctionCall.this.resolveBlocking(object).orNull(); + if (obj == null) { + throw new IllegalArgumentException("Deferred function call, " + object + + " evaluates to null (when calling " + fnName + "(" + DslDeferredFunctionCall.toString(args) + "))"); + } + return invokeOn(obj); + } + + }).build(); + } + + protected Maybe resolveImmediate(Object object) { + return resolve(object, true); + } + protected Maybe resolveBlocking(Object object) { + return resolve(object, false); + } + protected Maybe resolve(Object object, boolean immediate) { + if (object instanceof DslCallable || object == null) { + return Maybe.of(object); + } + Maybe resultMaybe = Tasks.resolving(object, Object.class) + .context(((EntityInternal)entity()).getExecutionContext()) + .deep(true) + .immediately(immediate) + .recursive(false) + .getMaybe(); + if (resultMaybe.isAbsent()) { + return resultMaybe; + } else { + // No nice way to figure out whether the object is deferred. Try to resolve it + // until it matches the input value as a poor man's replacement. + Object result = resultMaybe.get(); + if (result == object) { + return resultMaybe; + } else { + return resolve(result, immediate); + } + } + } + + protected Object invokeOn(Object obj) { + return invokeOn(obj, fnName, args); + } + + protected static Object invokeOn(Object obj, String fnName, List args) { + Object instance = obj; + List instanceArgs = args; + Maybe method = Reflections.getMethodFromArgs(instance, fnName, instanceArgs); + + if (method.isAbsent()) { + instance = BrooklynDslCommon.class; + instanceArgs = ImmutableList.builder().add(obj).addAll(args).build(); + method = Reflections.getMethodFromArgs(instance, fnName, instanceArgs); + } + + if (method.isAbsent()) { + Maybe facade; + try { + facade = Reflections.invokeMethodFromArgs(BrooklynDslCommon.DslFacades.class, "wrap", ImmutableList.of(obj)); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + facade = Maybe.absent(); + } + + if (facade.isPresent()) { + instance = facade.get(); + instanceArgs = args; + method = Reflections.getMethodFromArgs(instance, fnName, instanceArgs); + } + } + + if (method.isPresent()) { + Method m = method.get(); + + checkCallAllowed(m); + + try { + // Value is most likely another BrooklynDslDeferredSupplier - let the caller handle it, + return Reflections.invokeMethodFromArgs(instance, m, instanceArgs); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fnName+"("+toString(instanceArgs)+")' on '"+instance+"'")); + } + } else { + throw new IllegalArgumentException("No such function '"+fnName+"("+toString(args)+")' on "+obj); + } + } + + private static void checkCallAllowed(Method m) { + Class clazz = m.getDeclaringClass(); + if (clazz.getPackage() == null || // Proxy objects don't have a package + !(clazz.getPackage().getName().startsWith(BrooklynDslCommon.class.getPackage().getName()))) + throw new IllegalArgumentException("Not permitted to invoke function on '"+clazz+"' (outside allowed package scope)"); + } + + @Override + public int hashCode() { + return Objects.hashCode(object, fnName, args); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + DslDeferredFunctionCall that = DslDeferredFunctionCall.class.cast(obj); + return Objects.equal(this.object, that.object) && + Objects.equal(this.fnName, that.fnName) && + Objects.equal(this.args, that.args); + } + + @Override + public String toString() { + return object + "." + fnName + "(" + toString(args) + ")"; + } + + private static String toString(List args) { + if (args == null) return ""; + return Joiner.on(", ").join(args); + } +} 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 1bf93d7f85..bd2cbaf7f2 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 @@ -19,6 +19,7 @@ package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved; import java.util.Arrays; @@ -27,8 +28,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import javax.annotation.Nullable; - import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ExecutionContext; import org.apache.brooklyn.api.mgmt.Task; @@ -39,6 +38,8 @@ import org.apache.brooklyn.camp.brooklyn.spi.creation.EntitySpecConfiguration; import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.config.external.ExternalConfigSupplier; import org.apache.brooklyn.core.entity.EntityDynamicType; import org.apache.brooklyn.core.entity.EntityInternal; @@ -46,6 +47,8 @@ import org.apache.brooklyn.core.mgmt.internal.ExternalConfigSupplierRegistry; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider; +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.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; @@ -53,6 +56,7 @@ import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.core.flags.FlagUtils; import org.apache.brooklyn.util.core.task.DeferredSupplier; +import org.apache.brooklyn.util.core.task.ImmediateSupplier; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; @@ -69,7 +73,10 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; -/** static import functions which can be used in `$brooklyn:xxx` contexts */ +/** + * static import functions which can be used in `$brooklyn:xxx` contexts + * WARNING: Don't overload methods - the DSL evaluator will pick any one that matches, not the best match. + */ public class BrooklynDslCommon { private static final Logger LOG = LoggerFactory.getLogger(BrooklynDslCommon.class); @@ -120,6 +127,73 @@ public static BrooklynDslDeferredSupplier config(String keyName) { return new DslComponent(Scope.THIS, "").config(keyName); } + public static BrooklynDslDeferredSupplier config(BrooklynObjectInternal obj, String keyName) { + return new DslBrooklynObjectConfigSupplier(obj, keyName); + } + + public static class DslBrooklynObjectConfigSupplier extends BrooklynDslDeferredSupplier { + private static final long serialVersionUID = -2378555915585603381L; + + // Keep in mind this object gets serialized so is the following reference + private BrooklynObjectInternal obj; + private String keyName; + + public DslBrooklynObjectConfigSupplier(BrooklynObjectInternal obj, String keyName) { + checkNotNull(obj, "obj"); + checkNotNull(keyName, "keyName"); + + this.obj = obj; + this.keyName = keyName; + } + + @Override + public Maybe getImmediately() { + if (obj instanceof Entity) { + // Shouldn't worry too much about it since DSL can fetch objects from same app only. + // Just in case check whether it's same app for entities. + checkState(entity().getApplicationId().equals(((Entity)obj).getApplicationId())); + } + ConfigKey key = ConfigKeys.newConfigKey(Object.class, keyName); + Maybe result = ((AbstractConfigurationSupportInternal)obj.config()).getNonBlocking(key); + return Maybe.cast(result); + } + + @Override + public Task newTask() { + return Tasks.builder() + .displayName("retrieving config for "+keyName+" on "+obj) + .tag(BrooklynTaskTags.TRANSIENT_TASK_TAG) + .dynamic(false) + .body(new Callable() { + @Override + public Object call() throws Exception { + ConfigKey key = ConfigKeys.newConfigKey(Object.class, keyName); + return obj.getConfig(key); + }}) + .build(); + } + + @Override + public int hashCode() { + return Objects.hashCode(obj, keyName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + DslBrooklynObjectConfigSupplier that = DslBrooklynObjectConfigSupplier.class.cast(obj); + return Objects.equal(this.obj, that.obj) && + Objects.equal(this.keyName, that.keyName); + } + + @Override + public String toString() { + return (obj.toString()+".") + + "config("+JavaStringEscapes.wrapJavaString(keyName)+")"; + } + } + public static BrooklynDslDeferredSupplier attributeWhenReady(String sensorName) { return new DslComponent(Scope.THIS, "").attributeWhenReady(sensorName); } @@ -626,25 +700,17 @@ public String toString() { public static class Functions { public static Object regexReplacement(final Object pattern, final Object replacement) { if (resolved(pattern, replacement)) { - return new RegexReplacer(String.valueOf(pattern), String.valueOf(replacement)); + return new org.apache.brooklyn.util.text.StringFunctions.RegexReplacer(String.valueOf(pattern), String.valueOf(replacement)); } else { return new DslRegexReplacer(pattern, replacement); } } - public static class RegexReplacer implements Function { - private final String pattern; - private final String replacement; - + /** @deprecated since 0.11.0; use {@link org.apache.brooklyn.util.text.StringFunctions.RegexReplacer} instead */ + @Deprecated + public static class RegexReplacer extends org.apache.brooklyn.util.text.StringFunctions.RegexReplacer { public RegexReplacer(String pattern, String replacement) { - this.pattern = pattern; - this.replacement = replacement; - } - - @Nullable - @Override - public String apply(@Nullable String s) { - return s == null ? null : Strings.replaceAllRegex(s, pattern, replacement); + super(pattern, replacement); } } @@ -691,4 +757,40 @@ public String toString() { } } + // The results of the following methods are not supposed to get serialized. They are + // only intermediate values for the DSL evaluator to apply function calls on. There + // will always be a next method that gets executed on the return value. + public static class DslFacades { + private static class EntitySupplier implements DeferredSupplier, ImmediateSupplier { + private String entityId; + + public EntitySupplier(String entityId) { + this.entityId = entityId; + } + + @Override + public Maybe getImmediately() { + EntityInternal entity = entity(); + if (entity == null) { + return Maybe.absent(); + } + Entity targetEntity = entity.getManagementContext().getEntityManager().getEntity(entityId); + return Maybe.of(targetEntity); + } + + @Override + public Entity get() { + return getImmediately().orNull(); + } + + private EntityInternal entity() { + return (EntityInternal) BrooklynTaskTags.getTargetOrContextEntity(Tasks.current()); + } + } + + public static Object wrap(Entity entity) { + return DslComponent.newInstance(Scope.GLOBAL, new EntitySupplier(entity.getId())); + } + } + } diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java index 7274b2e5f7..d6859faf75 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java @@ -31,6 +31,7 @@ import org.apache.brooklyn.api.sensor.Sensor; import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants; import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslCallable; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.Entities; @@ -62,7 +63,7 @@ import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Callables; -public class DslComponent extends BrooklynDslDeferredSupplier { +public class DslComponent extends BrooklynDslDeferredSupplier implements DslCallable { private static final long serialVersionUID = -7715984495268724954L; @@ -527,7 +528,10 @@ public String toString() { "config("+JavaStringEscapes.wrapJavaString(keyName)+")"; } } - + + // TODO + // public BrooklynDslDeferredSupplier relation(BrooklynObjectInternal obj, final String relationName) {...} + public BrooklynDslDeferredSupplier> sensor(final Object sensorIndicator) { return new DslSensorSupplier(this, sensorIndicator); } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslTest.java similarity index 99% rename from camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslTest.java rename to camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslTest.java index ae5537929e..4292dbeeb0 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.brooklyn.camp.brooklyn.dsl; +package org.apache.brooklyn.camp.brooklyn.spi.dsl; import static com.google.common.base.Preconditions.checkNotNull; import static org.testng.Assert.assertEquals; @@ -24,7 +24,6 @@ import java.util.NoSuchElementException; import java.util.Random; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -33,13 +32,12 @@ import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants; -import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.EntityInternal; -import org.apache.brooklyn.core.objs.BasicSpecParameter; import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; +import org.apache.brooklyn.core.objs.BasicSpecParameter; import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestEntity; diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java new file mode 100644 index 0000000000..65fd66d5ba --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java @@ -0,0 +1,723 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed 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.brooklyn.spi.dsl; + +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.api.sensor.AttributeSensor; +import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslTestObjects.DslTestCallable; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslTestObjects.DslTestSupplierWrapper; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslTestObjects.TestDslSupplier; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslTestObjects.TestDslSupplierValue; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.custom.UserSuppliedPackageType; +import org.apache.brooklyn.config.ConfigKey; +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.test.entity.TestApplication; +import org.apache.brooklyn.entity.stock.BasicApplication; +import org.apache.brooklyn.entity.stock.BasicEntity; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.guava.Maybe; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; + +// Doesn't test executing the DSL from different contexts (i.e. fetching the config from children inheriting it) +public class DslYamlTest extends AbstractYamlTest { + private static final ConfigKey DEST = ConfigKeys.newConfigKey(Object.class, "dest"); + private static final ConfigKey DEST2 = ConfigKeys.newConfigKey(Object.class, "dest2"); + private static final ConfigKey DEST3 = ConfigKeys.newConfigKey(Object.class, "dest3"); + + // See also test-referencing-entities.yaml + + // No tests for entitySpec, object, formatString, external - relying on extensive tests elsewhere + + @Test + public void testDslSelf() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:self()"); + assertEquals(getConfigEventually(app, DEST), app); + } + + @Test + public void testDslEntity() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entity(\"child\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: child"); + assertEquals(getConfigEventually(app, DEST), Iterables.getOnlyElement(app.getChildren())); + } + + @Test + public void testDslParent() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:parent()"); + final Entity child = Iterables.getOnlyElement(app.getChildren()); + assertEquals(getConfigEventually(child, DEST), app); + } + + @Test + public void testDslChild() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:child(\"child\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: child", + " - type: " + BasicEntity.class.getName(), + " id: another-child"); + assertEquals(getConfigEventually(app, DEST), app.getChildren().iterator().next()); + } + + @Test + public void testDslSibling() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: child", + " brooklyn.config:", + " dest: $brooklyn:sibling(\"another-child\")", + " - type: " + BasicEntity.class.getName(), + " id: another-child"); + final Entity child1 = Iterables.get(app.getChildren(), 0); + final Entity child2 = Iterables.get(app.getChildren(), 1); + assertEquals(getConfigEventually(child1, DEST), child2); + } + + @Test + public void testDslDescendant() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " id: self", + " brooklyn.config:", + " dest: $brooklyn:descendant(\"child\")", + " dest2: $brooklyn:descendant(\"grand-child\")", + " dest3: $brooklyn:descendant(\"self\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: child", + " - type: " + BasicEntity.class.getName(), + " id: another-child", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: grand-child"); + final Entity child1 = Iterables.get(app.getChildren(), 0); + final Entity child2 = Iterables.get(app.getChildren(), 1); + final Entity grandChild = Iterables.getOnlyElement(child2.getChildren()); + assertEquals(getConfigEventually(app, DEST), child1); + assertEquals(getConfigEventually(app, DEST2), grandChild); + try { + assertEquals(getConfigEventually(app, DEST3), app); + Asserts.shouldHaveFailedPreviously("Self not in descendant scope"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "No entity matching id self"); + } + } + + @Test + public void testDslAncestor() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " id: app", + " brooklyn.config:", + " dest: $brooklyn:ancestor(\"app\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:ancestor(\"app\")", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:ancestor(\"app\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:ancestor(\"app\")"); + final Entity child1 = Iterables.get(app.getChildren(), 0); + final Entity child2 = Iterables.get(app.getChildren(), 1); + final Entity grandChild = Iterables.getOnlyElement(child2.getChildren()); + assertEquals(getConfigEventually(child1, DEST), app); + assertEquals(getConfigEventually(child2, DEST), app); + assertEquals(getConfigEventually(grandChild, DEST), app); + try { + assertEquals(getConfigEventually(app, DEST), app); + Asserts.shouldHaveFailedPreviously("App not in ancestor scope"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "No entity matching id app"); + } + } + + @Test + public void testDslRoot() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " id: app", + " brooklyn.config:", + " dest: $brooklyn:root()", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:root()", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:root()", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:root()"); + final Entity child1 = Iterables.get(app.getChildren(), 0); + final Entity child2 = Iterables.get(app.getChildren(), 1); + final Entity grandChild = Iterables.getOnlyElement(child2.getChildren()); + assertEquals(getConfigEventually(child1, DEST), app); + assertEquals(getConfigEventually(child2, DEST), app); + assertEquals(getConfigEventually(grandChild, DEST), app); + assertEquals(getConfigEventually(app, DEST), app); + } + + @Test + public void testDslScopeRoot() throws Exception { + addCatalogItems( + "brooklyn.catalog:", + " version: " + TEST_VERSION, + " items:", + " - id: simple-item", + " itemType: entity", + " item:", + " type: "+ BasicEntity.class.getName(), + " - id: wrapping-plain", + " itemType: entity", + " item:", + " type: "+ BasicEntity.class.getName(), + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:scopeRoot()", + " - id: wrapping-simple", + " itemType: entity", + " item:", + " type: "+ BasicEntity.class.getName(), + " brooklyn.children:", + " - type: simple-item", + " brooklyn.config:", + " dest: $brooklyn:scopeRoot()"); + + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.children:", + " - type: wrapping-plain", + " - type: wrapping-simple"); + Entity child1 = Iterables.get(app.getChildren(), 0); + Entity child2 = Iterables.get(app.getChildren(), 1); + assertScopeRoot(child1, false); + // TODO Not the result I'd expect - in both cases the entity argument should the the scopeRoot element, not its child + assertScopeRoot(child2, true); + } + + private void assertScopeRoot(Entity entity, boolean isScopeBugged) throws Exception { + Entity child = Iterables.getOnlyElement(entity.getChildren()); + if (!isScopeBugged) { + assertEquals(getConfigEventually(child, DEST), entity); + } else { + assertEquals(getConfigEventually(child, DEST), child); + } + } + + @Test + public void testDslConfig() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " source: myvalue", + " dest: $brooklyn:config(\"source\")"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test + public void testDslConfigOnEntity() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entity(\"sourceEntity\").config(\"source\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: sourceEntity", + " brooklyn.config:", + " source: myvalue"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test(groups="WIP") // config accepts strings only, no suppliers + public void testDslConfigWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " source: myvalue", + " configName: source", + " dest: $brooklyn:config(config(\"configName\"))"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test(groups="WIP") // config accepts strings only, no suppliers + public void testDslConfigOnEntityWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " entityName: sourceEntity", + " configName: source", + " dest: $brooklyn:entity(config(\"entityName\")).config(config(\"configName\"))", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: sourceEntity", + " brooklyn.config:", + " source: myvalue"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test + public void testDslAttributeWhenReady() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.StaticSensor", + " brooklyn.config:", + " name: source", + " static.value: myvalue", + " brooklyn.config:", + " dest: $brooklyn:attributeWhenReady(\"source\")"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test + public void testDslAttributeWhenReadyOnEntity() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entity(\"sourceEntity\").attributeWhenReady(\"source\")", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: sourceEntity", + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.StaticSensor", + " brooklyn.config:", + " name: source", + " static.value: myvalue"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test(groups="WIP") // config accepts strings only, no suppliers + public void testDslAttributeWhenReadyWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.StaticSensor", + " brooklyn.config:", + " name: source", + " static.value: myvalue", + " brooklyn.config:", + " configName: source", + " dest: $brooklyn:attributeWhenReady(config(\"configName\"))"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test(groups="WIP") // config accepts strings only, no suppliers + public void testDslAttributeWhenReadyOnEntityWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " entityName: sourceEntity", + " configName: source", + " dest: $brooklyn:entity(config(\"entityName\")).attributeWhenReady(config(\"configName\"))", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: sourceEntity", + " brooklyn.initializers:", + " - type: org.apache.brooklyn.core.sensor.StaticSensor", + " brooklyn.config:", + " name: source", + " static.value: myvalue"); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test + public void testDslEntityId() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entityId()"); + assertEquals(getConfigEventually(app, DEST), app.getId()); + } + + @Test + public void testDslEntityIdOnEntity() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entity(\"sourceEntity\").entityId()", + " brooklyn.children:", + " - type: " + BasicEntity.class.getName(), + " id: sourceEntity"); + final Entity child = Iterables.getOnlyElement(app.getChildren()); + assertEquals(getConfigEventually(app, DEST), child.getId()); + } + + @Test + public void testDslSensor() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:sensor(\"test.myattribute\")"); + assertEquals(getConfigEventually(app, DEST), TestApplication.MY_ATTRIBUTE); + } + + @Test + public void testDslSensorOnEntity() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:entity(\"sourceEntity\").sensor(\"test.myattribute\")", + " brooklyn.children:", + " - type: " + TestApplication.class.getName(), + " id: sourceEntity"); + assertEquals(getConfigEventually(app, DEST), TestApplication.MY_ATTRIBUTE); + } + + @Test + public void testDslSensorWithClass() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:sensor(\"org.apache.brooklyn.core.test.entity.TestApplication\", \"test.myattribute\")"); + assertEquals(getConfigEventually(app, DEST), TestApplication.MY_ATTRIBUTE); + } + + @Test + public void testDslLiteral() throws Exception { + final String literal = "custom(), $brooklyn:root(), invalid; syntax"; + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:literal(\"" + literal + "\")"); + assertEquals(getConfigEventually(app, DEST), literal); + } + + @Test + public void testDslRegexReplacement() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:regexReplacement(\"Broooklyn\", \"o+\", \"oo\")"); + assertEquals(getConfigEventually(app, DEST), "Brooklyn"); + } + + @Test + public void testDslRegexReplacementWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " source: Broooklyn", + " pattern: o+", + " replacement: oo", + " dest: $brooklyn:regexReplacement(config(\"source\"), config(\"pattern\"), config(\"replacement\"))"); + assertEquals(getConfigEventually(app, DEST), "Brooklyn"); + } + + @Test + public void testDslFunctionRegexReplacement() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:function.regexReplacement(\"o+\", \"oo\")"); + @SuppressWarnings("unchecked") + Function replacementFn = (Function) getConfigEventually(app, DEST); + assertEquals(replacementFn.apply("Broooklyn"), "Brooklyn"); + } + + @Test + public void testDslFunctionRegexReplacementWithDeferredArg() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + TestApplication.class.getName(), + " brooklyn.config:", + " source: Broooklyn", + " pattern: o+", + " replacement: oo", + " dest: $brooklyn:function.regexReplacement(config(\"pattern\"), config(\"replacement\"))"); + @SuppressWarnings("unchecked") + Function replacementFn = (Function) getConfigEventually(app, DEST); + assertEquals(replacementFn.apply("Broooklyn"), "Brooklyn"); + } + + public static class InaccessibleType { + public static void isEvaluated() {} + } + + @Test + public void testDeferredDslInaccessibleCall() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").isEvaluated()"); + app.config().set(ConfigKeys.newConfigKey(InaccessibleType.class, "targetValue"), new InaccessibleType()); + try { + getConfigEventually(app, DEST); + Asserts.shouldHaveFailedPreviously("Outside of allowed package scope"); + } catch (ExecutionException e) { + Asserts.expectedFailureContains(e, "(outside allowed package scope)"); + } + } + + @Test + public void testDeferredDslUserSuppliedPackage() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").isEvaluated()"); + app.config().set(ConfigKeys.newConfigKey(UserSuppliedPackageType.class, "targetValue"), new UserSuppliedPackageType()); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslChainingOnConfig() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").isSupplierEvaluated()"); + app.config().set(ConfigKeys.newConfigKey(TestDslSupplierValue.class, "targetValue"), new TestDslSupplierValue()); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslChainingOnConfigNoFunction() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").getNonExistent()"); + ConfigKey targetValueKey = ConfigKeys.newConfigKey(TestDslSupplierValue.class, "targetValue"); + app.config().set(targetValueKey, new TestDslSupplierValue()); + try { + assertEquals(getConfigEventually(app, DEST), app.getId()); + Asserts.shouldHaveFailedPreviously("Expected to fail because method does not exist"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "No such function 'getNonExistent()'"); + } + } + + @Test + public void testDeferredDslChainingOnSensor() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:attributeWhenReady(\"targetValue\").isSupplierEvaluated()"); + AttributeSensor targetValueSensor = Sensors.newSensor(TestDslSupplierValue.class, "targetValue"); + app.sensors().set(targetValueSensor, new TestDslSupplierValue()); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslObjectAsFirstArgument() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " location: localhost", + " brooklyn.config:", + " dest: $brooklyn:attributeWhenReady(\"targetValue\").config(\"spec.final\")"); + AttributeSensor targetValueSensor = Sensors.newSensor(Location.class, "targetValue"); + app.sensors().set(targetValueSensor, Iterables.getOnlyElement(app.getLocations())); + assertEquals(getConfigEventually(app, DEST), "localhost"); + } + + + @Test + public void testDeferredDslAttributeFacade() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:attributeWhenReady(\"targetEntity\").attributeWhenReady(\"entity.id\")"); + AttributeSensor targetEntitySensor = Sensors.newSensor(Entity.class, "targetEntity"); + app.sensors().set(targetEntitySensor, app); + assertEquals(getConfigEventually(app, DEST), app.getId()); + } + + @Test + public void testDeferredDslConfigFacade() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " testValue: myvalue", + " targetEntity: $brooklyn:self()", + " dest: $brooklyn:config(\"targetEntity\").config(\"testValue\")"); + AttributeSensor targetEntitySensor = Sensors.newSensor(Entity.class, "targetEntity"); + app.sensors().set(targetEntitySensor, app); + assertEquals(getConfigEventually(app, DEST), "myvalue"); + } + + @Test + public void testDeferredDslConfigFacadeCrossAppFails() throws Exception { + final Entity app0 = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName()); + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetEntity\").attributeWhenReady(\"entity.id\")"); + app.config().set(ConfigKeys.newConfigKey(Entity.class, "targetEntity"), app0); + try { + getConfigEventually(app, DEST); + Asserts.shouldHaveFailedPreviously("Cross-app DSL not allowed"); + } catch (ExecutionException e) { + Asserts.expectedFailureContains(e, "not in scope 'global'"); + } + } + + @Test + public void testDeferredDslAttributeFacadeCrossAppFails() throws Exception { + final Entity app0 = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName()); + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:attributeWhenReady(\"targetEntity\").attributeWhenReady(\"entity.id\")"); + AttributeSensor targetEntitySensor = Sensors.newSensor(Entity.class, "targetEntity"); + app.sensors().set(targetEntitySensor, app0); + try { + getConfigEventually(app, DEST); + Asserts.shouldHaveFailedPreviously("Cross-app DSL not allowed"); + } catch (ExecutionException e) { + Asserts.expectedFailureContains(e, "not in scope 'global'"); + } + } + + @Test + public void testDeferredDslChainingOnNullConfig() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetEntity\").getId()"); + try { + assertEquals(getConfigEventually(app, DEST), app.getId()); + Asserts.shouldHaveFailedPreviously("Expected to fail because targetEntity config is null"); + } catch (Exception e) { + Asserts.expectedFailureContains(e, "config(\"targetEntity\") evaluates to null"); + } + } + + @Test + public void testDeferredDslChainingWithCustomSupplier() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"customSupplierWrapper\").getSupplier().isSupplierEvaluated()"); + ConfigKey customSupplierWrapperKey = ConfigKeys.newConfigKey(DslTestSupplierWrapper.class, "customSupplierWrapper"); + app.config().set(customSupplierWrapperKey, new DslTestSupplierWrapper(new TestDslSupplier(new TestDslSupplierValue()))); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslChainingWithCustomCallable() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"customCallableWrapper\").getSupplier().isSupplierCallable()"); + ConfigKey customCallableWrapperKey = ConfigKeys.newConfigKey(DslTestSupplierWrapper.class, "customCallableWrapper"); + app.config().set(customCallableWrapperKey, new DslTestSupplierWrapper(new DslTestCallable())); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslChainingWithNestedEvaluation() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"customCallableWrapper\").getSupplier().isSupplierCallable()"); + ConfigKey customCallableWrapperKey = ConfigKeys.newConfigKey(TestDslSupplier.class, "customCallableWrapper"); + app.config().set(customCallableWrapperKey, new TestDslSupplier(new DslTestSupplierWrapper(new DslTestCallable()))); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + private static T getConfigEventually(final Entity entity, final ConfigKey configKey) throws Exception { + Task result = ((EntityInternal)entity).getExecutionContext().submit(new Callable() { + @Override + public T call() throws Exception { + // TODO Move the getNonBlocking call out of the task after #280 is merged. + // Currently doesn't work because no execution context available. + Maybe immediateValue = ((EntityInternal)entity).config().getNonBlocking(configKey); + T blockingValue = entity.config().get(configKey); + assertEquals(immediateValue.get(), blockingValue); + return blockingValue; + } + }); + return result.get(Asserts.DEFAULT_LONG_TIMEOUT); + } +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslTestObjects.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslTestObjects.java new file mode 100644 index 0000000000..9cd1dbddb4 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslTestObjects.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed 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.brooklyn.spi.dsl.methods; + +import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslCallable; +import org.apache.brooklyn.util.core.task.DeferredSupplier; +import org.apache.brooklyn.util.core.task.ImmediateSupplier; +import org.apache.brooklyn.util.guava.Maybe; + +public class DslTestObjects { + + public static class DslTestSupplierWrapper { + private Object supplier; + + public DslTestSupplierWrapper(Object supplier) { + this.supplier = supplier; + } + + public Object getSupplier() { + return supplier; + } + } + + public static class TestDslSupplierValue { + public boolean isSupplierEvaluated() { + return true; + } + } + + public static class TestDslSupplier implements DeferredSupplier, ImmediateSupplier { + private Object value; + + public TestDslSupplier(Object value) { + this.value = value; + } + + @Override + public Object get() { + return getImmediately().get(); + } + + @Override + public Maybe getImmediately() { + return Maybe.of(value); + } + } + + public static class DslTestCallable implements DslCallable, DeferredSupplier, ImmediateSupplier { + + @Override + public Maybe getImmediately() { + throw new IllegalStateException("Not to be called"); + } + + @Override + public TestDslSupplier get() { + throw new IllegalStateException("Not to be called"); + } + + public boolean isSupplierCallable() { + return true; + } + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/custom/UserSuppliedPackageType.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/custom/UserSuppliedPackageType.java new file mode 100644 index 0000000000..bef1cd88bd --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/custom/UserSuppliedPackageType.java @@ -0,0 +1,22 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed 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.brooklyn.spi.dsl.methods.custom; + +public class UserSuppliedPackageType { + public boolean isEvaluated() { + return true; + } +} 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 6775a44132..4dcf5be88c 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 @@ -31,6 +31,7 @@ import org.apache.brooklyn.api.mgmt.TaskAdaptable; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; +import org.apache.brooklyn.core.mgmt.rebind.ImmediateDeltaChangeListener; import org.apache.brooklyn.util.core.flags.TypeCoercions; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; @@ -110,6 +111,7 @@ public class ValueResolver implements DeferredSupplier { /** timeout on execution, if possible, or if embedResolutionInTask is true */ Duration timeout; boolean immediately; + boolean recursive = true; boolean isTransientTask = true; T defaultValue = null; @@ -144,6 +146,7 @@ public class ValueResolver implements DeferredSupplier { timeout = parent.timeout; immediately = parent.immediately; + // not copying recursive as we want deep resolving to be recursive, only top-level values should be non-recursive parentTimer = parent.parentTimer; if (parentTimer!=null && parentTimer.isExpired()) expired = true; @@ -167,7 +170,9 @@ public ValueResolver clone() { .context(exec).description(description) .embedResolutionInTask(embedResolutionInTask) .deep(forceDeep) - .timeout(timeout); + .timeout(timeout) + .immediately(immediately) + .recursive(recursive); if (returnDefaultOnGet) result.defaultValue(defaultValue); if (swallowExceptions) result.swallowExceptions(); return result; @@ -264,6 +269,18 @@ public ValueResolver immediately(boolean val) { return this; } + /** + * Whether the value should be resolved recursively. When true the result of + * the resolving will be resolved again recursively until the value is an immediate object. + * When false will try to resolve the value a single time and return the result even if it + * can be resolved further (e.x. it is DeferredSupplier). + */ + @Beta + public ValueResolver recursive(boolean val) { + this.recursive = val; + return this; + } + protected void checkTypeNotNull() { if (type==null) throw new NullPointerException("type must be set to resolve, for '"+value+"'"+(description!=null ? ", "+description : "")); @@ -297,6 +314,11 @@ protected Maybe getMaybeInternal() { exec = BasicExecutionContext.getCurrentExecutionContext(); } + if (!recursive && type != Object.class) { + throw new IllegalStateException("When non-recursive resolver requested the return type must be Object " + + "as the immediately resolved value could be a number of (deferred) types."); + } + CountdownTimer timerU = parentTimer; if (timerU==null && timeout!=null) timerU = timeout.countdownTimer(); @@ -319,7 +341,11 @@ protected Maybe getMaybeInternal() { Maybe result = supplier.getImmediately(); // Recurse: need to ensure returned value is cast, etc - return (result.isPresent()) ? new ValueResolver(result.get(), type, this).getMaybe() : Maybe.absent(); + return (result.isPresent()) + ? recursive + ? new ValueResolver(result.get(), type, this).getMaybe() + : result + : Maybe.absent(); } catch (ImmediateSupplier.ImmediateUnsupportedException e) { log.debug("Unable to resolve-immediately for "+description+" ("+v+"); falling back to executing with timeout", e); } @@ -455,7 +481,12 @@ public Object call() throws Exception { throw problem; } - return new ValueResolver(v, type, this).getMaybe(); + if (recursive) { + return new ValueResolver(v, type, this).getMaybe(); + } else { + // T expected to be Object.class + return (Maybe) Maybe.of(v); + } } protected String getDescription() { diff --git a/core/src/main/resources/org/apache/brooklyn/core/mgmt/persist/deserializingClassRenames.properties b/core/src/main/resources/org/apache/brooklyn/core/mgmt/persist/deserializingClassRenames.properties index 300bba2ecf..6d6fa61919 100644 --- a/core/src/main/resources/org/apache/brooklyn/core/mgmt/persist/deserializingClassRenames.properties +++ b/core/src/main/resources/org/apache/brooklyn/core/mgmt/persist/deserializingClassRenames.properties @@ -1436,3 +1436,6 @@ org.apache.brooklyn.config.ConfigInheritance$Always org.apache.brooklyn.config.ConfigInheritance$Merged : org.apache.brooklyn.config.ConfigInheritance$Legacy$Merged org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks$ProvisioningTaskState : org.apache.brooklyn.core.entity.internal.AttributesInternal$ProvisioningTaskState + +# since 0.11.0 +org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon.Functions.RegexReplacer : org.apache.brooklyn.util.text.StringFunctions.RegexReplacer diff --git a/core/src/test/java/org/apache/brooklyn/util/core/task/ValueResolverTest.java b/core/src/test/java/org/apache/brooklyn/util/core/task/ValueResolverTest.java index b7e908523f..ffe6762a6c 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/task/ValueResolverTest.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/task/ValueResolverTest.java @@ -172,7 +172,55 @@ public void testGetImmediatelyFallsBackToDeferredCallInTask() throws Exception { assertEquals(BrooklynTaskTags.getContextEntity(callInfo.task), app); assertNotContainsCallingMethod(callInfo.stackTrace, "testGetImmediatelyFallsBackToDeferredCallInTask"); } - + + public void testNonRecursiveBlockingFailsOnNonObjectType() throws Exception { + try { + Tasks.resolving(new WrappingImmediateAndDeferredSupplier(new FailingImmediateAndDeferredSupplier())) + .as(FailingImmediateAndDeferredSupplier.class) + .context(app) + .immediately(false) + .recursive(false) + .get(); + Asserts.shouldHaveFailedPreviously("recursive(true) accepts only as(Object.class)"); + } catch (IllegalStateException e) { + Asserts.expectedFailureContains(e, "must be Object"); + } + } + + public void testNonRecursiveBlocking() throws Exception { + Object result = Tasks.resolving(new WrappingImmediateAndDeferredSupplier(new FailingImmediateAndDeferredSupplier())) + .as(Object.class) + .context(app) + .immediately(false) + .recursive(false) + .get(); + assertEquals(result.getClass(), FailingImmediateAndDeferredSupplier.class); + } + + public void testNonRecursiveImmediateFailsOnNonObjectType() throws Exception { + try { + Tasks.resolving(new WrappingImmediateAndDeferredSupplier(new FailingImmediateAndDeferredSupplier())) + .as(FailingImmediateAndDeferredSupplier.class) + .context(app) + .immediately(true) + .recursive(false) + .get(); + Asserts.shouldHaveFailedPreviously("recursive(true) accepts only as(Object.class)"); + } catch (IllegalStateException e) { + Asserts.expectedFailureContains(e, "must be Object"); + } + } + + public void testNonRecursiveImmediately() throws Exception { + Object result = Tasks.resolving(new WrappingImmediateAndDeferredSupplier(new FailingImmediateAndDeferredSupplier())) + .as(Object.class) + .context(app) + .immediately(true) + .recursive(false) + .get(); + assertEquals(result.getClass(), FailingImmediateAndDeferredSupplier.class); + } + private static class MyImmediateAndDeferredSupplier implements ImmediateSupplier, DeferredSupplier { private final boolean failImmediately; @@ -198,6 +246,39 @@ public CallInfo get() { } } + private static class WrappingImmediateAndDeferredSupplier implements ImmediateSupplier, DeferredSupplier { + private Object value; + + public WrappingImmediateAndDeferredSupplier(Object value) { + this.value = value; + } + + @Override + public Object get() { + return getImmediately().get(); + } + + @Override + public Maybe getImmediately() { + return Maybe.of(value); + } + + } + + private static class FailingImmediateAndDeferredSupplier implements ImmediateSupplier, DeferredSupplier { + + @Override + public Object get() { + throw new IllegalStateException("Not to be called"); + } + + @Override + public Maybe getImmediately() { + throw new IllegalStateException("Not to be called"); + } + + } + private static class CallInfo { final StackTraceElement[] stackTrace; final Task task; 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 95bbf7fa6a..fba72b5741 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 @@ -842,18 +842,26 @@ public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String } /** as {@link #invokeMethodFromArgs(Object, String, List)} but giving control over whether to set it accessible */ public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String method, List args, boolean setAccessible) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + Maybe maybeMethod = getMethodFromArgs(clazzOrInstance, method, args); + if (maybeMethod.isAbsent()) { + return Maybe.absent(Maybe.getException(maybeMethod)); + } + Method m = maybeMethod.get(); + + return Maybe.of(invokeMethodFromArgs(clazzOrInstance, m, args, setAccessible)); + } + + /** searches for the given method on the given clazz or instance, doing reasonably good matching on args etc */ + public static Maybe getMethodFromArgs(Object clazzOrInstance, String method, List args) { Preconditions.checkNotNull(clazzOrInstance, "clazz or instance"); Preconditions.checkNotNull(method, "method"); Preconditions.checkNotNull(args, "args to "+method); Class clazz; - Object instance; if (clazzOrInstance instanceof Class) { clazz = (Class)clazzOrInstance; - instance = null; } else { clazz = clazzOrInstance.getClass(); - instance = clazzOrInstance; } Object[] argsArray = args.toArray(); @@ -873,25 +881,16 @@ public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String } } if (varargsMatch) { - Object varargs = Array.newInstance(varargType, argsArray.length+1 - parameterTypes.length); - for (int i=parameterTypes.length-1; i argTypes = Lists.newArrayList(); for (Object arg : args) { argTypes.add(arg == null ? "" : arg.getClass().getSimpleName()); @@ -899,6 +898,49 @@ public static Maybe invokeMethodFromArgs(Object clazzOrInstance, String return Maybe.absent("Method '"+method+"' not found matching given args of type "+argTypes); } + /** invokes the given method on the given clazz or instance, assuming that the method matches passed arguments + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws IllegalArgumentException */ + public static Object invokeMethodFromArgs(Object clazzOrInstance, Method m, List args) + throws IllegalAccessException, InvocationTargetException { + return invokeMethodFromArgs(clazzOrInstance, m, args, false); + } + + /** as {@link #invokeMethodFromArgs(Object, Method, List)} but giving control over whether to set it accessible */ + public static Object invokeMethodFromArgs(Object clazzOrInstance, Method m, List args, boolean setAccessible) + throws IllegalAccessException, InvocationTargetException { + Preconditions.checkNotNull(clazzOrInstance, "clazz or instance"); + Preconditions.checkNotNull(m, "method"); + Preconditions.checkNotNull(args, "args to "+m); + + Object instance; + if (clazzOrInstance instanceof Class) { + instance = null; + } else { + instance = clazzOrInstance; + } + + Object[] argsArray = args.toArray(); + + Class[] parameterTypes = m.getParameterTypes(); + if (m.isVarArgs()) { + Class varargType = parameterTypes[parameterTypes.length-1].getComponentType(); + Object varargs = Array.newInstance(varargType, argsArray.length+1 - parameterTypes.length); + for (int i=parameterTypes.length-1; i[] parameterTypes) { if (argsArray.length != parameterTypes.length) diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java index ec2c023f6d..c9ec2a6963 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/StringFunctions.java @@ -26,6 +26,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Functions; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; @@ -409,7 +410,22 @@ public RegexReplacer(String pattern, String replacement) { @Nullable @Override public String apply(@Nullable String s) { - return Strings.replaceAllRegex(s, pattern, replacement); + return s == null ? null : Strings.replaceAllRegex(s, pattern, replacement); } + + @Override + public int hashCode() { + return Objects.hashCode(pattern, replacement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + RegexReplacer that = RegexReplacer.class.cast(obj); + return Objects.equal(this.pattern, that.pattern) && + Objects.equal(this.replacement, that.replacement); + } + } } 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 b6bb63c798..b7d397513f 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 @@ -128,6 +128,24 @@ public void testInvocation() throws Exception { Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3, 4, 5)).get(), "hello12"); } + @Test + public void testMethodInvocation() throws Exception { + Method m1Short = CI1.class.getMethod("m1", String.class, int.class); + Method m1Long = CI1.class.getMethod("m1", String.class, int.class, int.class, int[].class); + + Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, m1Short, Arrays.asList("hello", 3)), "hello3"); + Assert.assertEquals(Reflections.invokeMethodFromArgs(CI1.class, m1Long, Arrays.asList("hello", 3, 4, 5)), "hello12"); + } + + @Test + public void testGetMethod() throws Exception { + Method m1Short = CI1.class.getMethod("m1", String.class, int.class); + Method m1Long = CI1.class.getMethod("m1", String.class, int.class, int.class, int[].class); + + Assert.assertEquals(Reflections.getMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3)).get(), m1Short); + Assert.assertEquals(Reflections.getMethodFromArgs(CI1.class, "m1", Arrays.asList("hello", 3, 4, 5)).get(), m1Long); + } + @Test public void testConstruction() throws Exception { Assert.assertEquals(Reflections.invokeConstructorFromArgs(CI1.class, new Object[] {"hello", 3}).get().constructorArgs, ImmutableList.of("hello", 3));