From 2e0ee180db8713a8251c68538a63550c2c29a537 Mon Sep 17 00:00:00 2001 From: Svetoslav Neykov Date: Thu, 8 Dec 2016 11:27:04 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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(