From 2e0ee180db8713a8251c68538a63550c2c29a537 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Thu, 8 Dec 2016 11:27:04 +0200 Subject: [PATCH 01/11] Basic coverage of DSL YAML parsing and evaluation --- .../brooklyn/dsl/DslYamlBlockingTest.java | 673 ++++++++++++++++++ 1 file changed, 673 insertions(+) create mode 100644 camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java new file mode 100644 index 0000000000..280b060ce2 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java @@ -0,0 +1,673 @@ +/* + * 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.dsl; + +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.Callable; + +import org.apache.brooklyn.api.entity.Entity; +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.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.core.task.DeferredSupplier; +import org.apache.brooklyn.util.core.task.ImmediateSupplier; +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 DslYamlBlockingTest 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"); + } + + @Test + public void testDeferredDslChainingOnConfig() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " targetEntity: $brooklyn:self()", + " dest: $brooklyn:config(\"targetEntity\").getId()"); + assertEquals(getConfigEventually(app, DEST), app.getId()); + } + + @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(\"targetEntity\").getId()"); + AttributeSensor targetEntitySensor = Sensors.newSensor(Entity.class, "targetEntity"); + app.sensors().set(targetEntitySensor, app); + assertEquals(getConfigEventually(app, DEST), app.getId()); + } + + @Test(groups="WIP") + public void testDeferredDslWrapsIntermediates() 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 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"); + } + } + + 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); + } + } + + @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); + } + + public static class DslTestCallable implements 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; + } + } + + @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); + } +} From d71141ef995351ff172ade6e00f6f251a4479ee9 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Thu, 8 Dec 2016 12:29:51 +0200 Subject: [PATCH 02/11] Deferred DSL evaluation, allowing for chaining --- .../spi/dsl/BrooklynDslInterpreter.java | 24 ++-- .../camp/brooklyn/spi/dsl/DslCallable.java | 26 ++++ .../spi/dsl/DslDeferredFunctionCall.java | 124 ++++++++++++++++++ .../spi/dsl/methods/DslComponent.java | 3 +- .../brooklyn/dsl/DslYamlBlockingTest.java | 3 +- 5 files changed, 167 insertions(+), 13 deletions(-) create mode 100644 camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslCallable.java create mode 100644 camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java 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..5e61b4303b 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 */ @@ -175,19 +174,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..20d1a4edf2 --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java @@ -0,0 +1,124 @@ +/* + * 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.util.List; +import java.util.concurrent.Callable; + +import org.apache.brooklyn.api.mgmt.Task; +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; + +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 = object.getImmediately(); + 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 = object.get(); + if (obj == null) { + throw new IllegalArgumentException("Deferred function call, " + object + + " evaluates to null (when calling " + fnName + "(" + DslDeferredFunctionCall.toString(args) + "))"); + } + return invokeOn(obj); + } + + }).build(); + } + + protected Object invokeOn(Object obj) { + return invokeOn(obj, fnName, args); + } + + protected static Object invokeOn(Object obj, String fnName, List args) { + Maybe v; + try { + v = Reflections.invokeMethodFromArgs(obj, fnName, args); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + Exceptions.propagateIfFatal(e); + throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fnName+"("+toString(args)+")' on '"+obj+"'")); + } + if (v.isPresent()) { + // Value is most likely another BrooklynDslDeferredSupplier - let the caller handle it, + return v.get(); + } else { + throw new IllegalArgumentException("No such function '"+fnName+"("+toString(args)+")' on "+obj); + } + } + + @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/DslComponent.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java index 7274b2e5f7..ea8e818099 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; diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java index 280b060ce2..772a28f8d4 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java @@ -23,6 +23,7 @@ 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.DslCallable; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.EntityInternal; @@ -615,7 +616,7 @@ public void testDeferredDslChainingWithCustomSupplier() throws Exception { assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); } - public static class DslTestCallable implements DeferredSupplier, ImmediateSupplier { + public static class DslTestCallable implements DslCallable, DeferredSupplier, ImmediateSupplier { @Override public Maybe getImmediately() { From 498a58c05766374f2fa6484432122b766c1b3ef7 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Mon, 12 Dec 2016 16:38:31 +0200 Subject: [PATCH 03/11] Deprecate BDC.Functions.RegexReplacer - duplicate of StringFunctions.RegexReplacer --- .../spi/dsl/methods/BrooklynDslCommon.java | 19 ++++++------------- .../deserializingClassRenames.properties | 3 +++ .../brooklyn/util/text/StringFunctions.java | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 14 deletions(-) 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..36b274c178 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 @@ -58,6 +58,7 @@ import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; +import org.apache.brooklyn.util.text.StringFunctions.RegexReplacer; import org.apache.brooklyn.util.text.Strings; import org.apache.commons.beanutils.BeanUtils; import org.slf4j.Logger; @@ -626,25 +627,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); } } 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/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); + } + } } From ddfe73361d4fb123c91758c2f48fd722247d54e2 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Mon, 12 Dec 2016 16:40:57 +0200 Subject: [PATCH 04/11] Tighten DSL function origin check, apply on deferred --- .../brooklyn/spi/dsl/BrooklynDslInterpreter.java | 9 --------- .../brooklyn/spi/dsl/DslDeferredFunctionCall.java | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 9 deletions(-) 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 5e61b4303b..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 @@ -156,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.")) { 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 index 20d1a4edf2..7a31ee0d3b 100644 --- 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 @@ -20,6 +20,7 @@ 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.mgmt.BrooklynTaskTags; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.exceptions.Exceptions; @@ -82,6 +83,8 @@ protected Object invokeOn(Object obj) { } protected static Object invokeOn(Object obj, String fnName, List args) { + checkCallAllowed(obj, fnName, args); + Maybe v; try { v = Reflections.invokeMethodFromArgs(obj, fnName, args); @@ -97,6 +100,17 @@ protected static Object invokeOn(Object obj, String fnName, List args) { } } + private static void checkCallAllowed(Object obj, String fnName2, List args2) { + Class clazz; + if (obj instanceof Class) { + clazz = (Class)obj; + } else { + clazz = obj.getClass(); + } + if (!(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); From 42bc7c136ec885cc43125edfee86a741793aa64b Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Mon, 12 Dec 2016 18:02:16 +0200 Subject: [PATCH 05/11] Non-recursive ValueResolver --- .../util/core/task/ValueResolver.java | 37 ++++++++- .../util/core/task/ValueResolverTest.java | 83 ++++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) 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/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; From e831d75471554f57709f14889c7b0ff78dec6e63 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Mon, 12 Dec 2016 18:03:35 +0200 Subject: [PATCH 06/11] Stop recursive resolving of DSL results on DslCallable Lets us call methods on DslCallable methods and at the same time resolve the object if it's the last of the chain. --- .../spi/dsl/DslDeferredFunctionCall.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) 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 index 7a31ee0d3b..9835be6b42 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -47,7 +48,7 @@ public DslDeferredFunctionCall(BrooklynDslDeferredSupplier o, String fn, List @Override public Maybe getImmediately() { - Maybe obj = object.getImmediately(); + Maybe obj = resolveImmediate(object); if (obj.isPresent()) { if (obj.isNull()) { throw new IllegalArgumentException("Deferred function call, " + object + @@ -67,7 +68,7 @@ public Task newTask() { .body(new Callable() { @Override public Object call() throws Exception { - Object obj = object.get(); + 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) + "))"); @@ -78,6 +79,36 @@ public Object call() throws Exception { }).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); } From 6607ccbc3adff741c23d1aff7fb13fa9e6e16a8d Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Tue, 13 Dec 2016 08:52:29 +0200 Subject: [PATCH 07/11] Move test objects in the allowed package scope Fixes tests which expect the DSL evaluated objects to be in the correct package. Adds tests to verify that objects outside of the package scope are inaccessible to DSL. --- .../camp/brooklyn/{ => spi}/dsl/DslTest.java | 6 +- .../dsl/DslYamlTest.java} | 111 ++++++++---------- .../spi/dsl/methods/DslTestObjects.java | 78 ++++++++++++ .../custom/UserSuppliedPackageType.java | 22 ++++ 4 files changed, 148 insertions(+), 69 deletions(-) rename camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/{ => spi}/dsl/DslTest.java (99%) rename camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/{dsl/DslYamlBlockingTest.java => spi/dsl/DslYamlTest.java} (91%) create mode 100644 camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslTestObjects.java create mode 100644 camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/custom/UserSuppliedPackageType.java 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/dsl/DslYamlBlockingTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java similarity index 91% rename from camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java rename to camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java index 772a28f8d4..d266ed2808 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/dsl/DslYamlBlockingTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java @@ -13,17 +13,22 @@ * 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 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.mgmt.Task; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; -import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslCallable; +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; @@ -32,8 +37,6 @@ 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.core.task.DeferredSupplier; -import org.apache.brooklyn.util.core.task.ImmediateSupplier; import org.apache.brooklyn.util.guava.Maybe; import org.testng.annotations.Test; @@ -41,7 +44,7 @@ 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 DslYamlBlockingTest extends AbstractYamlTest { +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"); @@ -501,15 +504,46 @@ public void testDslFunctionRegexReplacementWithDeferredArg() throws Exception { 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:", - " targetEntity: $brooklyn:self()", - " dest: $brooklyn:config(\"targetEntity\").getId()"); - assertEquals(getConfigEventually(app, DEST), app.getId()); + " dest: $brooklyn:config(\"targetValue\").isSupplierEvaluated()"); + app.config().set(ConfigKeys.newConfigKey(TestDslSupplierValue.class, "targetValue"), new TestDslSupplierValue()); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); } @Test @@ -535,10 +569,10 @@ public void testDeferredDslChainingOnSensor() throws Exception { "services:", "- type: " + BasicApplication.class.getName(), " brooklyn.config:", - " dest: $brooklyn:attributeWhenReady(\"targetEntity\").getId()"); - AttributeSensor targetEntitySensor = Sensors.newSensor(Entity.class, "targetEntity"); - app.sensors().set(targetEntitySensor, app); - assertEquals(getConfigEventually(app, DEST), app.getId()); + " 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(groups="WIP") @@ -568,42 +602,6 @@ public void testDeferredDslChainingOnNullConfig() throws Exception { } } - 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); - } - } - @Test public void testDeferredDslChainingWithCustomSupplier() throws Exception { final Entity app = createAndStartApplication( @@ -616,23 +614,6 @@ public void testDeferredDslChainingWithCustomSupplier() throws Exception { assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); } - 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; - } - } - @Test public void testDeferredDslChainingWithCustomCallable() throws Exception { final Entity app = createAndStartApplication( 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; + } +} From eb4ac4c1493a563982c98eb8cbd97e47bf9a6700 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Tue, 13 Dec 2016 11:11:41 +0200 Subject: [PATCH 08/11] Split Reflections.invokeMethodArgs into Method find and Method invoke Gives more control over what method gets executed. --- .../brooklyn/util/javalang/Reflections.java | 72 +++++++++++++++---- .../util/javalang/ReflectionsTest.java | 18 +++++ 2 files changed, 75 insertions(+), 15 deletions(-) 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/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)); From b75b45c77aeb51ee8549ae0aab9f6bf040daa935 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Tue, 13 Dec 2016 12:06:30 +0200 Subject: [PATCH 09/11] Provide mixins/facade DSL methods Allows us to call custom DSL methods on any object we are interested in without the object knowing about DSL. --- .../spi/dsl/BrooklynDslDeferredSupplier.java | 1 - .../spi/dsl/DslDeferredFunctionCall.java | 62 +++++++--- .../spi/dsl/methods/BrooklynDslCommon.java | 117 +++++++++++++++++- .../spi/dsl/methods/DslComponent.java | 5 +- .../camp/brooklyn/spi/dsl/DslYamlTest.java | 72 ++++++++++- 5 files changed, 229 insertions(+), 28 deletions(-) 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/DslDeferredFunctionCall.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredFunctionCall.java index 9835be6b42..ad8c4b154c 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -30,6 +31,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; public class DslDeferredFunctionCall extends BrooklynDslDeferredSupplier { @@ -114,31 +116,51 @@ protected Object invokeOn(Object obj) { } protected static Object invokeOn(Object obj, String fnName, List args) { - checkCallAllowed(obj, fnName, args); - - Maybe v; - try { - v = Reflections.invokeMethodFromArgs(obj, fnName, args); - } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { - Exceptions.propagateIfFatal(e); - throw Exceptions.propagate(new InvocationTargetException(e, "Error invoking '"+fnName+"("+toString(args)+")' on '"+obj+"'")); + 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 (v.isPresent()) { - // Value is most likely another BrooklynDslDeferredSupplier - let the caller handle it, - return v.get(); + + 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(Object obj, String fnName2, List args2) { - Class clazz; - if (obj instanceof Class) { - clazz = (Class)obj; - } else { - clazz = obj.getClass(); - } - if (!(clazz.getPackage().getName().startsWith(BrooklynDslCommon.class.getPackage().getName()))) + 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)"); } @@ -161,7 +183,7 @@ public boolean equals(Object obj) { 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 36b274c178..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,12 +56,12 @@ 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; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes; -import org.apache.brooklyn.util.text.StringFunctions.RegexReplacer; import org.apache.brooklyn.util.text.Strings; import org.apache.commons.beanutils.BeanUtils; import org.slf4j.Logger; @@ -70,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); @@ -121,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); } @@ -684,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 ea8e818099..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 @@ -528,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/spi/dsl/DslYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java index d266ed2808..65fd66d5ba 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -575,8 +576,22 @@ public void testDeferredDslChainingOnSensor() throws Exception { assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); } - @Test(groups="WIP") - public void testDeferredDslWrapsIntermediates() throws Exception { + @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(), @@ -587,6 +602,59 @@ public void testDeferredDslWrapsIntermediates() throws Exception { 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( From 60c5c6fa97ae3bd33548832123e5f50c5532b128 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Tue, 13 Dec 2016 16:42:57 +0200 Subject: [PATCH 10/11] Rename DslCallable to DslFunctionSource and some cleanup Addresses review comments. --- .../spi/dsl/BrooklynDslInterpreter.java | 7 +- .../spi/dsl/DslDeferredFunctionCall.java | 95 ++++++++++--------- ...slCallable.java => DslFunctionSource.java} | 2 +- .../spi/dsl/methods/DslComponent.java | 4 +- .../camp/brooklyn/spi/dsl/DslYamlTest.java | 32 ++++++- .../spi/dsl/methods/DslTestObjects.java | 7 +- .../util/core/task/ValueResolver.java | 1 - 7 files changed, 90 insertions(+), 58 deletions(-) rename camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/{DslCallable.java => DslFunctionSource.java} (96%) 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 16d5c5a389..ab20c3b239 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 @@ -33,7 +33,6 @@ 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; @@ -170,12 +169,14 @@ public Object evaluateOn(Object o, FunctionWithArgs f, boolean deepEvaluation) { args.add( deepEvaluation ? evaluate(arg, true) : arg ); } try { - if (o instanceof BrooklynDslDeferredSupplier && !(o instanceof DslCallable)) { + // TODO Could move argument resolve in DslDeferredFunctionCall freeing each Deffered implementation + // having to handle it separately. The shortcoming is that will lose the eager evaluation we have here. + if (o instanceof BrooklynDslDeferredSupplier && !(o instanceof DslFunctionSource)) { 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); + return DslDeferredFunctionCall.invokeOn(o, fn, args).get(); } } catch (Exception e) { Exceptions.propagateIfFatal(e); 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 index ad8c4b154c..638f36f8b3 100644 --- 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 @@ -37,12 +37,11 @@ public class DslDeferredFunctionCall extends BrooklynDslDeferredSupplier private static final long serialVersionUID = 3243262633795112155L; - // TODO should this be some of the super types? - private BrooklynDslDeferredSupplier object; + private Object object; private String fnName; private List args; - public DslDeferredFunctionCall(BrooklynDslDeferredSupplier o, String fn, List args) { + public DslDeferredFunctionCall(Object o, String fn, List args) { this.object = o; this.fnName = fn; this.args = args; @@ -50,15 +49,7 @@ public DslDeferredFunctionCall(BrooklynDslDeferredSupplier o, String fn, List @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); + return invokeOnDeferred(object, true); } @Override @@ -70,52 +61,37 @@ public Task newTask() { .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); + return invokeOnDeferred(object, false).get(); } }).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; + protected Maybe invokeOnDeferred(Object obj, boolean immediate) { + Maybe resolvedMaybe = resolve(obj, immediate); + if (resolvedMaybe.isPresent()) { + Object instance = resolvedMaybe.get(); + + if (instance == null) { + throw new IllegalArgumentException("Deferred function call, " + object + + " evaluates to null (when calling " + fnName + "(" + toString(args) + "))"); + } + + return invokeOn(instance); } 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; + if (immediate) { + return Maybe.absent("Could not evaluate immediately " + obj); } else { - return resolve(result, immediate); + return Maybe.absent(Maybe.getException(resolvedMaybe)); } } } - protected Object invokeOn(Object obj) { + protected Maybe invokeOn(Object obj) { return invokeOn(obj, fnName, args); } - protected static Object invokeOn(Object obj, String fnName, List args) { + protected static Maybe invokeOn(Object obj, String fnName, List args) { Object instance = obj; List instanceArgs = args; Maybe method = Reflections.getMethodFromArgs(instance, fnName, instanceArgs); @@ -148,12 +124,39 @@ protected static Object invokeOn(Object obj, String fnName, List args) { try { // Value is most likely another BrooklynDslDeferredSupplier - let the caller handle it, - return Reflections.invokeMethodFromArgs(instance, m, instanceArgs); + return Maybe.of(Reflections.invokeMethodFromArgs(instance, m, instanceArgs)); } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + // If the method is there but not executable for whatever reason fail with a fatal error, don't return an absent. 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); + return Maybe.absent(new IllegalArgumentException("No such function '"+fnName+"("+toString(args)+")' on "+obj)); + } + } + + protected Maybe resolve(Object object, boolean immediate) { + if (object instanceof DslFunctionSource || 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.isPresent()) { + // 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); + } + } else { + return resultMaybe; } } 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/DslFunctionSource.java similarity index 96% rename from camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslCallable.java rename to camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslFunctionSource.java index 4eeddb747e..ac856036eb 100644 --- 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/DslFunctionSource.java @@ -21,6 +21,6 @@ * 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 { +public interface DslFunctionSource { } 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 d6859faf75..37b9c59697 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,7 +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.camp.brooklyn.spi.dsl.DslFunctionSource; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.Entities; @@ -63,7 +63,7 @@ import com.google.common.collect.Iterables; import com.google.common.util.concurrent.Callables; -public class DslComponent extends BrooklynDslDeferredSupplier implements DslCallable { +public class DslComponent extends BrooklynDslDeferredSupplier implements DslFunctionSource { private static final long serialVersionUID = -7715984495268724954L; 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 index 65fd66d5ba..aca1abaa19 100644 --- 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 @@ -38,6 +38,7 @@ 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.exceptions.CompoundRuntimeException; import org.apache.brooklyn.util.guava.Maybe; import org.testng.annotations.Test; @@ -351,7 +352,7 @@ public void testDslAttributeWhenReadyOnEntity() throws Exception { assertEquals(getConfigEventually(app, DEST), "myvalue"); } - @Test(groups="WIP") // config accepts strings only, no suppliers + @Test(groups="WIP") // attributeWhenReady accepts strings only, no suppliers public void testDslAttributeWhenReadyWithDeferredArg() throws Exception { final Entity app = createAndStartApplication( "services:", @@ -367,7 +368,7 @@ public void testDslAttributeWhenReadyWithDeferredArg() throws Exception { assertEquals(getConfigEventually(app, DEST), "myvalue"); } - @Test(groups="WIP") // config accepts strings only, no suppliers + @Test(groups="WIP") // attributeWhenReady accepts strings only, no suppliers public void testDslAttributeWhenReadyOnEntityWithDeferredArg() throws Exception { final Entity app = createAndStartApplication( "services:", @@ -505,6 +506,20 @@ public void testDslFunctionRegexReplacementWithDeferredArg() throws Exception { assertEquals(replacementFn.apply("Broooklyn"), "Brooklyn"); } + @Test + public void testDslNonDeferredInvalidMethod() throws Exception { + try { + createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:self().invalidMethod()"); + Asserts.shouldHaveFailedPreviously("Non-existing non-deferred method should fail deployment"); + } catch (CompoundRuntimeException e) { + Asserts.expectedFailureContains(e, "No such function 'invalidMethod()'"); + } + } + public static class InaccessibleType { public static void isEvaluated() {} } @@ -547,6 +562,17 @@ public void testDeferredDslChainingOnConfig() throws Exception { assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); } + @Test + public void testDeferredDslChainingDslComponent() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").self().attributeWhenReady(\"entity.id\")"); + app.config().set(ConfigKeys.newConfigKey(TestDslSupplierValue.class, "targetValue"), new TestDslSupplierValue()); + assertEquals(getConfigEventually(app, DEST), app.getId()); + } + @Test public void testDeferredDslChainingOnConfigNoFunction() throws Exception { final Entity app = createAndStartApplication( @@ -710,7 +736,7 @@ private static T getConfigEventually(final Entity entity, final ConfigKey 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. + // TODO Move the getNonBlocking call out of the task after #480 is merged. // Currently doesn't work because no execution context available. Maybe immediateValue = ((EntityInternal)entity).config().getNonBlocking(configKey); T blockingValue = entity.config().get(configKey); 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 index 9cd1dbddb4..60225aff51 100644 --- 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 @@ -15,7 +15,7 @@ */ package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods; -import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslCallable; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslFunctionSource; import org.apache.brooklyn.util.core.task.DeferredSupplier; import org.apache.brooklyn.util.core.task.ImmediateSupplier; import org.apache.brooklyn.util.guava.Maybe; @@ -38,6 +38,9 @@ public static class TestDslSupplierValue { public boolean isSupplierEvaluated() { return true; } + public DslComponent self() { + return BrooklynDslCommon.self(); + } } public static class TestDslSupplier implements DeferredSupplier, ImmediateSupplier { @@ -58,7 +61,7 @@ public Maybe getImmediately() { } } - public static class DslTestCallable implements DslCallable, DeferredSupplier, ImmediateSupplier { + public static class DslTestCallable implements DslFunctionSource, DeferredSupplier, ImmediateSupplier { @Override public Maybe getImmediately() { 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 4dcf5be88c..3b00252b11 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,7 +31,6 @@ 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; From f564aa813a311f9b06d9ab4ef5142875f971d50a Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Tue, 13 Dec 2016 17:18:24 +0200 Subject: [PATCH 11/11] DslAccessible replaces hard-coded package DSL white-listing Applying the DslAccessible annotation on any method allows it to be called by DSL --- .../camp/brooklyn/spi/dsl/DslAccessible.java | 28 +++++++++++++++++++ .../spi/dsl/DslDeferredFunctionCall.java | 27 ++++++++++++++++-- .../spi/dsl/methods/BrooklynDslCommon.java | 26 +++++++++++++++++ .../spi/dsl/methods/DslComponent.java | 16 +++++++++++ .../camp/brooklyn/spi/dsl/DslYamlTest.java | 28 +++++++++++++++++-- .../spi/dsl/methods/DslTestObjects.java | 5 ++++ 6 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslAccessible.java diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslAccessible.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslAccessible.java new file mode 100644 index 0000000000..6c9de73848 --- /dev/null +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslAccessible.java @@ -0,0 +1,28 @@ +/* + * 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 java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface DslAccessible { + +} 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 index 638f36f8b3..3353151190 100644 --- 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 @@ -17,8 +17,11 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon; @@ -28,12 +31,16 @@ import org.apache.brooklyn.util.exceptions.Exceptions; 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.Joiner; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; public class DslDeferredFunctionCall extends BrooklynDslDeferredSupplier { + private static final Logger log = LoggerFactory.getLogger(DslDeferredFunctionCall.class); + private static final Set DEPRECATED_ACCESS_WARNINGS = Collections.newSetFromMap(new ConcurrentHashMap()); private static final long serialVersionUID = 3243262633795112155L; @@ -161,10 +168,24 @@ protected Maybe resolve(Object object, boolean immediate) { } private static void checkCallAllowed(Method m) { + DslAccessible dslAccessible = m.getAnnotation(DslAccessible.class); + boolean isAnnotationAllowed = dslAccessible != null; + if (isAnnotationAllowed) return; + + // TODO white-list using brooklyn.properties (at runtime) + 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)"); + Package whiteListPackage = BrooklynDslCommon.class.getPackage(); + boolean isPackageAllowed = (clazz.getPackage() != null && // Proxy objects don't have a package + clazz.getPackage().getName().startsWith(whiteListPackage.getName())); + if (isPackageAllowed) { + if (DEPRECATED_ACCESS_WARNINGS.add(m)) { + log.warn("Deprecated since 0.11.0. The method '" + m.toString() + "' called by DSL should be white listed using the " + DslAccessible.class.getSimpleName() + " annotation. Support for DSL callable methods under the " + whiteListPackage + " will be fremoved in a future release."); + } + return; + } + + throw new IllegalArgumentException("Not permitted to invoke function on '"+clazz+"' (outside allowed package scope)"); } @Override 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 bd2cbaf7f2..c17382d9b7 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 @@ -37,6 +37,7 @@ import org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynYamlTypeInstantiator; 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.DslAccessible; import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; @@ -83,37 +84,48 @@ public class BrooklynDslCommon { // Access specific entities + @DslAccessible public static DslComponent self() { return new DslComponent(Scope.THIS); } + @DslAccessible public static DslComponent entity(Object id) { return DslComponent.newInstance(Scope.GLOBAL, id); } + @DslAccessible public static DslComponent parent() { return new DslComponent(Scope.PARENT); } + @DslAccessible public static DslComponent child(Object id) { return DslComponent.newInstance(Scope.CHILD, id); } + @DslAccessible public static DslComponent sibling(Object id) { return DslComponent.newInstance(Scope.SIBLING, id); } + @DslAccessible public static DslComponent descendant(Object id) { return DslComponent.newInstance(Scope.DESCENDANT, id); } + @DslAccessible public static DslComponent ancestor(Object id) { return DslComponent.newInstance(Scope.ANCESTOR, id); } + @DslAccessible public static DslComponent root() { return new DslComponent(Scope.ROOT); } + @DslAccessible public static DslComponent scopeRoot() { return new DslComponent(Scope.SCOPE_ROOT); } // prefer the syntax above to the below now, but not deprecating the below + @DslAccessible public static DslComponent component(String id) { return component("global", id); } + @DslAccessible public static DslComponent component(String scope, String id) { if (!DslComponent.Scope.isValid(scope)) { throw new IllegalArgumentException(scope + " is not a valid scope"); @@ -123,10 +135,12 @@ public static DslComponent component(String scope, String id) { // Access things on entities + @DslAccessible public static BrooklynDslDeferredSupplier config(String keyName) { return new DslComponent(Scope.THIS, "").config(keyName); } + @DslAccessible public static BrooklynDslDeferredSupplier config(BrooklynObjectInternal obj, String keyName) { return new DslBrooklynObjectConfigSupplier(obj, keyName); } @@ -194,22 +208,26 @@ public String toString() { } } + @DslAccessible public static BrooklynDslDeferredSupplier attributeWhenReady(String sensorName) { return new DslComponent(Scope.THIS, "").attributeWhenReady(sensorName); } + @DslAccessible public static BrooklynDslDeferredSupplier entityId() { return new DslComponent(Scope.THIS, "").entityId(); } /** Returns a {@link Sensor}, looking up the sensor on the context if available and using that, * or else defining an untyped (Object) sensor */ + @DslAccessible public static BrooklynDslDeferredSupplier> sensor(Object sensorName) { return new DslComponent(Scope.THIS, "").sensor(sensorName); } /** Returns a {@link Sensor} declared on the type (e.g. entity class) declared in the first argument. */ @SuppressWarnings({ "unchecked", "rawtypes" }) + @DslAccessible 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?! @@ -238,6 +256,7 @@ public static Sensor sensor(String clazzName, String sensorName) { // Build complex things + @DslAccessible public static EntitySpecConfiguration entitySpec(Map arguments) { return new EntitySpecConfiguration(arguments); } @@ -249,6 +268,7 @@ public static EntitySpecConfiguration entitySpec(Map arguments) * bundles). */ @SuppressWarnings("unchecked") + @DslAccessible public static Object object(Map arguments) { ConfigBag config = ConfigBag.newInstance(arguments); String typeName = BrooklynYamlTypeInstantiator.InstantiatorFromKey.extractTypeName("object", config).orNull(); @@ -285,6 +305,7 @@ public static Object object(Map arguments) { // String manipulation /** Return the expression as a literal string without any further parsing. */ + @DslAccessible public static Object literal(Object expression) { return expression; } @@ -293,6 +314,7 @@ public static Object literal(Object expression) { * Returns a formatted string or a {@link BrooklynDslDeferredSupplier} if the arguments * are not yet fully resolved. */ + @DslAccessible public static Object formatString(final String pattern, final Object...args) { if (resolved(args)) { // if all args are resolved, apply the format string now @@ -302,6 +324,7 @@ public static Object formatString(final String pattern, final Object...args) { } } + @DslAccessible public static Object regexReplacement(final Object source, final Object pattern, final Object replacement) { if (resolved(Arrays.asList(source, pattern, replacement))) { return (new Functions.RegexReplacer(String.valueOf(pattern), String.valueOf(replacement))).apply(String.valueOf(source)); @@ -642,6 +665,7 @@ public String toString() { * The name of the appropriate {@link ExternalConfigSupplier} is captured, along with the key of * the desired config value. */ + @DslAccessible public static DslExternal external(final String providerName, final String key) { return new DslExternal(providerName, key); } @@ -698,6 +722,7 @@ public String toString() { } public static class Functions { + @DslAccessible public static Object regexReplacement(final Object pattern, final Object replacement) { if (resolved(pattern, replacement)) { return new org.apache.brooklyn.util.text.StringFunctions.RegexReplacer(String.valueOf(pattern), String.valueOf(replacement)); @@ -788,6 +813,7 @@ private EntityInternal entity() { } } + @DslAccessible 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 37b9c59697..a947feebf4 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.DslAccessible; import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslFunctionSource; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; @@ -324,41 +325,52 @@ private ExecutionContext getExecutionContext() { // DSL words which move to a new component + @DslAccessible public DslComponent entity(Object id) { return DslComponent.newInstance(this, Scope.GLOBAL, id); } + @DslAccessible public DslComponent child(Object id) { return DslComponent.newInstance(this, Scope.CHILD, id); } + @DslAccessible public DslComponent sibling(Object id) { return DslComponent.newInstance(this, Scope.SIBLING, id); } + @DslAccessible public DslComponent descendant(Object id) { return DslComponent.newInstance(this, Scope.DESCENDANT, id); } + @DslAccessible public DslComponent ancestor(Object id) { return DslComponent.newInstance(this, Scope.ANCESTOR, id); } + @DslAccessible public DslComponent root() { return new DslComponent(this, Scope.ROOT); } + @DslAccessible public DslComponent scopeRoot() { return new DslComponent(this, Scope.SCOPE_ROOT); } @Deprecated /** @deprecated since 0.7.0 */ + @DslAccessible public DslComponent component(Object id) { return DslComponent.newInstance(this, Scope.GLOBAL, id); } + @DslAccessible public DslComponent self() { return new DslComponent(this, Scope.THIS); } + @DslAccessible public DslComponent parent() { return new DslComponent(this, Scope.PARENT); } + @DslAccessible public DslComponent component(String scope, Object id) { if (!DslComponent.Scope.isValid(scope)) { throw new IllegalArgumentException(scope + " is not a valid scope"); @@ -368,6 +380,7 @@ public DslComponent component(String scope, Object id) { // DSL words which return things + @DslAccessible public BrooklynDslDeferredSupplier entityId() { return new EntityId(this); } @@ -411,6 +424,7 @@ public String toString() { } } + @DslAccessible public BrooklynDslDeferredSupplier attributeWhenReady(final String sensorName) { return new AttributeWhenReady(this, sensorName); } @@ -468,6 +482,7 @@ public String toString() { } } + @DslAccessible public BrooklynDslDeferredSupplier config(final String keyName) { return new DslConfigSupplier(this, keyName); } @@ -532,6 +547,7 @@ public String toString() { // TODO // public BrooklynDslDeferredSupplier relation(BrooklynObjectInternal obj, final String relationName) {...} + @DslAccessible 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/spi/dsl/DslYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslYamlTest.java index aca1abaa19..2196df9c69 100644 --- 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 @@ -521,7 +521,9 @@ public void testDslNonDeferredInvalidMethod() throws Exception { } public static class InaccessibleType { - public static void isEvaluated() {} + public static boolean doesFail() {return true;} + @DslAccessible + public static boolean doesSucceed() {return true;} } @Test @@ -530,7 +532,7 @@ public void testDeferredDslInaccessibleCall() throws Exception { "services:", "- type: " + BasicApplication.class.getName(), " brooklyn.config:", - " dest: $brooklyn:config(\"targetValue\").isEvaluated()"); + " dest: $brooklyn:config(\"targetValue\").doesFail()"); app.config().set(ConfigKeys.newConfigKey(InaccessibleType.class, "targetValue"), new InaccessibleType()); try { getConfigEventually(app, DEST); @@ -540,6 +542,28 @@ public void testDeferredDslInaccessibleCall() throws Exception { } } + @Test + public void testDeferredDslAccessible() throws Exception { + final Entity app = createAndStartApplication( + "services:", + "- type: " + BasicApplication.class.getName(), + " brooklyn.config:", + " dest: $brooklyn:config(\"targetValue\").doesSucceed()"); + app.config().set(ConfigKeys.newConfigKey(InaccessibleType.class, "targetValue"), new InaccessibleType()); + assertEquals(getConfigEventually(app, DEST), Boolean.TRUE); + } + + @Test + public void testDeferredDslWhiteListPackage() 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 testDeferredDslUserSuppliedPackage() throws Exception { final Entity app = createAndStartApplication( 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 index 60225aff51..3918ebdd0e 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods; +import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible; import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslFunctionSource; import org.apache.brooklyn.util.core.task.DeferredSupplier; import org.apache.brooklyn.util.core.task.ImmediateSupplier; @@ -29,15 +30,18 @@ public DslTestSupplierWrapper(Object supplier) { this.supplier = supplier; } + @DslAccessible public Object getSupplier() { return supplier; } } public static class TestDslSupplierValue { + @DslAccessible public boolean isSupplierEvaluated() { return true; } + @DslAccessible public DslComponent self() { return BrooklynDslCommon.self(); } @@ -73,6 +77,7 @@ public TestDslSupplier get() { throw new IllegalStateException("Not to be called"); } + @DslAccessible public boolean isSupplierCallable() { return true; }