You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2016/11/11 11:56:21 UTC

[3/4] brooklyn-server git commit: BROOKLYN-381: nested DSL support in $brooklyn:entity()

BROOKLYN-381: nested DSL support in $brooklyn:entity()

For example, $brooklyn:entity(attributeWhenReady(\u201ctargetId\u201d))

Also tests this in the context of a TestHttpCall, in TestHttpCallTest


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/62a295cc
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/62a295cc
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/62a295cc

Branch: refs/heads/master
Commit: 62a295cc6967ed507f041365f91bd1fbfd377d8e
Parents: d7b4459
Author: Aled Sage <al...@gmail.com>
Authored: Tue Nov 8 15:59:39 2016 +0000
Committer: Aled Sage <al...@gmail.com>
Committed: Fri Nov 11 09:30:22 2016 +0000

----------------------------------------------------------------------
 .../spi/dsl/methods/BrooklynDslCommon.java      |  31 ++--
 .../brooklyn/spi/dsl/methods/DslComponent.java  | 175 ++++++++++++++++---
 .../camp/brooklyn/DslAndRebindYamlTest.java     |  74 ++++++++
 .../framework/yaml/TestHttpCallYamlTest.java    |  21 +++
 4 files changed, 258 insertions(+), 43 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/62a295cc/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
----------------------------------------------------------------------
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 8fb48cf..1bf93d7 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
@@ -39,7 +39,6 @@ import org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynYamlTypeInstantiat
 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.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;
@@ -53,8 +52,8 @@ import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.ClassLoaderUtils;
 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.Tasks;
-import org.apache.brooklyn.util.core.task.ValueResolver;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.javalang.Reflections;
@@ -78,31 +77,31 @@ public class BrooklynDslCommon {
     // Access specific entities
 
     public static DslComponent self() {
-        return new DslComponent(Scope.THIS, null);
+        return new DslComponent(Scope.THIS);
     }
-    public static DslComponent entity(String id) {
-        return new DslComponent(Scope.GLOBAL, id);
+    public static DslComponent entity(Object id) {
+        return DslComponent.newInstance(Scope.GLOBAL, id);
     }
     public static DslComponent parent() {
-        return new DslComponent(Scope.PARENT, null);
+        return new DslComponent(Scope.PARENT);
     }
-    public static DslComponent child(String id) {
-        return new DslComponent(Scope.CHILD, id);
+    public static DslComponent child(Object id) {
+        return DslComponent.newInstance(Scope.CHILD, id);
     }
-    public static DslComponent sibling(String id) {
-        return new DslComponent(Scope.SIBLING, id);
+    public static DslComponent sibling(Object id) {
+        return DslComponent.newInstance(Scope.SIBLING, id);
     }
-    public static DslComponent descendant(String id) {
-        return new DslComponent(Scope.DESCENDANT, id);
+    public static DslComponent descendant(Object id) {
+        return DslComponent.newInstance(Scope.DESCENDANT, id);
     }
-    public static DslComponent ancestor(String id) {
-        return new DslComponent(Scope.ANCESTOR, id);
+    public static DslComponent ancestor(Object id) {
+        return DslComponent.newInstance(Scope.ANCESTOR, id);
     }
     public static DslComponent root() {
-        return new DslComponent(Scope.ROOT, null);
+        return new DslComponent(Scope.ROOT);
     }
     public static DslComponent scopeRoot() {
-        return new DslComponent(Scope.SCOPE_ROOT, null);
+        return new DslComponent(Scope.SCOPE_ROOT);
     }
     // prefer the syntax above to the below now, but not deprecating the below
     public static DslComponent component(String id) {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/62a295cc/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslComponent.java
----------------------------------------------------------------------
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 b2f20d3..1bd20eb 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
@@ -18,6 +18,8 @@
  */
 package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods;
 
+import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
+
 import java.util.NoSuchElementException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -38,6 +40,9 @@ import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
 import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal;
 import org.apache.brooklyn.core.sensor.DependentConfiguration;
 import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.task.BasicExecutionContext;
+import org.apache.brooklyn.util.core.task.DeferredSupplier;
 import org.apache.brooklyn.util.core.task.ImmediateSupplier;
 import org.apache.brooklyn.util.core.task.TaskBuilder;
 import org.apache.brooklyn.util.core.task.Tasks;
@@ -45,6 +50,7 @@ import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.groovy.GroovyJavaMethods;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+import org.apache.brooklyn.util.text.Strings;
 
 import com.google.common.base.CaseFormat;
 import com.google.common.base.Converter;
@@ -61,10 +67,39 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
     private static final long serialVersionUID = -7715984495268724954L;
     
     private final String componentId;
+    private final DeferredSupplier<?> componentIdSupplier;
     private final DslComponent scopeComponent;
     private final Scope scope;
 
     /**
+     * Checks the type of {@code componentId} to create the right kind of {@link DslComponent}
+     * (based on whether the componentId is already resolved. Accepts either a {@link String} or a 
+     * {@link DeferredSupplier}.
+     */
+    public static DslComponent newInstance(DslComponent scopeComponent, Scope scope, Object componentId) {
+        if (resolved(componentId)) {
+            // if all args are resolved, apply the componentId now
+            return new DslComponent(scopeComponent, scope, (String) componentId);
+        } else {
+            return new DslComponent(scopeComponent, scope, (DeferredSupplier<?>)componentId);
+        }
+    }
+
+    /**
+     * Checks the type of {@code componentId} to create the right kind of {@link DslComponent}
+     * (based on whether the componentId is already resolved. Accepts either a {@link String} or a 
+     * {@link DeferredSupplier}.
+     */
+    public static DslComponent newInstance(Scope scope, Object componentId) {
+        if (resolved(componentId)) {
+            // if all args are resolved, apply the componentId now
+            return new DslComponent(scope, (String) componentId);
+        } else {
+            return new DslComponent(scope, (DeferredSupplier<?>)componentId);
+        }
+    }
+
+    /**
      * Resolve componentId in the {@link Scope#GLOBAL} scope.
      * 
      * @deprecated since 0.10.0; pass the {@link Scope} explicitly.
@@ -75,6 +110,25 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
     }
 
     /**
+     * Resolve in scope relative to the current 
+     * {@link BrooklynTaskTags#getTargetOrContextEntity) target or context} entity
+     * (where the scope defines an unambiguous relationship that will resolve to a single 
+     * component - e.g. "parent").
+     */
+    public DslComponent(Scope scope) {
+        this(null, scope);
+    }
+
+    /**
+     * Resolve in scope relative to {@code scopeComponent} entity
+     * (where the scope defines an unambiguous relationship that will resolve to a single 
+     * component - e.g. "parent").
+     */
+    public DslComponent(DslComponent scopeComponent, Scope scope) {
+        this(scopeComponent, scope, (String)null);
+    }
+    
+    /**
      * Resolve componentId in scope relative to the current
      * {@link BrooklynTaskTags#getTargetOrContextEntity) target or context} entity.
      */
@@ -82,6 +136,10 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
         this(null, scope, componentId);
     }
 
+    public DslComponent(Scope scope, DeferredSupplier<?> componentIdSupplier) {
+        this(null, scope, componentIdSupplier);
+    }
+
     /**
      * Resolve componentId in scope relative to scopeComponent.
      */
@@ -89,6 +147,18 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
         Preconditions.checkNotNull(scope, "scope");
         this.scopeComponent = scopeComponent;
         this.componentId = componentId;
+        this.componentIdSupplier = null;
+        this.scope = scope;
+    }
+
+    /**
+     * Resolve componentId in scope relative to scopeComponent.
+     */
+    public DslComponent(DslComponent scopeComponent, Scope scope, DeferredSupplier<?> componentIdSupplier) {
+        Preconditions.checkNotNull(scope, "scope");
+        this.scopeComponent = scopeComponent;
+        this.componentId = null;
+        this.componentIdSupplier = componentIdSupplier;
         this.scope = scope;
     }
 
@@ -96,7 +166,7 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
 
     @Override
     public final Maybe<Entity> getImmediately() {
-        return new EntityInScopeFinder(scopeComponent, scope, componentId).getImmediately();
+        return new EntityInScopeFinder(scopeComponent, scope, componentId, componentIdSupplier).getImmediately();
     }
 
     @Override
@@ -104,7 +174,7 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
         return TaskBuilder.<Entity>builder()
                 .displayName(toString())
                 .tag(BrooklynTaskTags.TRANSIENT_TASK_TAG)
-                .body(new EntityInScopeFinder(scopeComponent, scope, componentId))
+                .body(new EntityInScopeFinder(scopeComponent, scope, componentId, componentIdSupplier))
                 .build();
     }
     
@@ -112,11 +182,13 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
         protected final DslComponent scopeComponent;
         protected final Scope scope;
         protected final String componentId;
-
-        public EntityInScopeFinder(DslComponent scopeComponent, Scope scope, String componentId) {
+        protected final DeferredSupplier<?> componentIdSupplier;
+        
+        public EntityInScopeFinder(DslComponent scopeComponent, Scope scope, String componentId, DeferredSupplier<?> componentIdSupplier) {
             this.scopeComponent = scopeComponent;
             this.scope = scope;
             this.componentId = componentId;
+            this.componentIdSupplier = componentIdSupplier;
         }
 
         @Override 
@@ -185,63 +257,112 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
                     throw new IllegalStateException("Unexpected scope "+scope);
             }
             
-            Optional<Entity> result = Iterables.tryFind(entitiesToSearch, EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, componentId));
+            String desiredComponentId;
+            if (componentId == null) {
+                if (componentIdSupplier == null) {
+                    throw new IllegalArgumentException("No component-id or component-id supplier, when resolving entity in scope '" + scope + "' wrt " + entity);
+                }
+                
+                Maybe<Object> maybeComponentId = Tasks.resolving(componentIdSupplier)
+                        .as(Object.class)
+                        .context(getExecutionContext())
+                        .immediately(immediate)
+                        .description("Resolving component-id from " + componentIdSupplier)
+                        .getMaybe();
+                
+                if (immediate) {
+                    if (maybeComponentId.isAbsent()) {
+                        return Maybe.absent(Maybe.getException(maybeComponentId));
+                    }
+                }
+                
+                // Support being passes an explicit entity via the DSL
+                if (maybeComponentId.get() instanceof Entity) {
+                    if (Iterables.contains(entitiesToSearch, maybeComponentId.get())) {
+                        return Maybe.of((Entity)maybeComponentId.get());
+                    } else {
+                        throw new IllegalStateException("Resolved component " + maybeComponentId.get() + " is not in scope '" + scope + "' wrt " + entity);
+                    }
+                }
+                
+                desiredComponentId = TypeCoercions.coerce(maybeComponentId.get(), String.class);
+
+                if (Strings.isBlank(desiredComponentId)) {
+                    throw new IllegalStateException("component-id blank, from " + componentIdSupplier);
+                }
+                
+            } else {
+                desiredComponentId = componentId;
+            }
+            
+            Optional<Entity> result = Iterables.tryFind(entitiesToSearch, EntityPredicates.configEqualTo(BrooklynCampConstants.PLAN_ID, desiredComponentId));
             
             if (result.isPresent()) {
                 return Maybe.of(result.get());
             }
             
             // TODO may want to block and repeat on new entities joining?
-            throw new NoSuchElementException("No entity matching id " + componentId+
+            throw new NoSuchElementException("No entity matching id " + desiredComponentId+
                 (scope==Scope.GLOBAL ? "" : ", in scope "+scope+" wrt "+entity+
                 (scopeComponent!=null ? " ("+scopeComponent+" from "+entity()+")" : "")));
-        }        
+        }
+        
+        private ExecutionContext getExecutionContext() {
+            EntityInternal contextEntity = (EntityInternal) BrooklynTaskTags.getTargetOrContextEntity(Tasks.current());
+            ExecutionContext execContext =
+                    (contextEntity != null) ? contextEntity.getExecutionContext()
+                                            : BasicExecutionContext.getCurrentExecutionContext();
+            if (execContext == null) {
+                throw new IllegalStateException("No execution context available to resolve " + toString());
+            }
+            return execContext;
+        }
     }
     
     // -------------------------------
 
     // DSL words which move to a new component
     
-    public DslComponent entity(String scopeOrId) {
-        return new DslComponent(this, Scope.GLOBAL, scopeOrId);
+    public DslComponent entity(Object id) {
+        return DslComponent.newInstance(this, Scope.GLOBAL, id);
     }
-    public DslComponent child(String scopeOrId) {
-        return new DslComponent(this, Scope.CHILD, scopeOrId);
+    public DslComponent child(Object id) {
+        return DslComponent.newInstance(this, Scope.CHILD, id);
     }
-    public DslComponent sibling(String scopeOrId) {
-        return new DslComponent(this, Scope.SIBLING, scopeOrId);
+    public DslComponent sibling(Object id) {
+        return DslComponent.newInstance(this, Scope.SIBLING, id);
     }
-    public DslComponent descendant(String scopeOrId) {
-        return new DslComponent(this, Scope.DESCENDANT, scopeOrId);
+    public DslComponent descendant(Object id) {
+        return DslComponent.newInstance(this, Scope.DESCENDANT, id);
     }
-    public DslComponent ancestor(String scopeOrId) {
-        return new DslComponent(this, Scope.ANCESTOR, scopeOrId);
+    public DslComponent ancestor(Object id) {
+        return DslComponent.newInstance(this, Scope.ANCESTOR, id);
     }
     public DslComponent root() {
-        return new DslComponent(this, Scope.ROOT, "");
+        return new DslComponent(this, Scope.ROOT);
     }
     public DslComponent scopeRoot() {
-        return new DslComponent(this, Scope.SCOPE_ROOT, "");
+        return new DslComponent(this, Scope.SCOPE_ROOT);
     }
     
     @Deprecated /** @deprecated since 0.7.0 */
-    public DslComponent component(String scopeOrId) {
-        return new DslComponent(this, Scope.GLOBAL, scopeOrId);
+    public DslComponent component(Object id) {
+        return DslComponent.newInstance(this, Scope.GLOBAL, id);
     }
     
     public DslComponent self() {
-        return new DslComponent(this, Scope.THIS, null);
+        return new DslComponent(this, Scope.THIS);
     }
     
     public DslComponent parent() {
-        return new DslComponent(this, Scope.PARENT, "");
+        return new DslComponent(this, Scope.PARENT);
     }
     
-    public DslComponent component(String scope, String id) {
+    public DslComponent component(String scope, Object id) {
         if (!DslComponent.Scope.isValid(scope)) {
-            throw new IllegalArgumentException(scope + " is not a vlaid scope");
+            throw new IllegalArgumentException(scope + " is not a valid scope");
         }
-        return new DslComponent(this, DslComponent.Scope.fromString(scope), id);
+        return DslComponent.newInstance(this, DslComponent.Scope.fromString(scope), id);
     }
 
     // DSL words which return things
@@ -564,7 +685,7 @@ public class DslComponent extends BrooklynDslDeferredSupplier<Entity> {
         return "$brooklyn:entity("+
             (scopeComponent==null ? "" : JavaStringEscapes.wrapJavaString(scopeComponent.toString())+", ")+
             (scope==Scope.GLOBAL ? "" : JavaStringEscapes.wrapJavaString(scope.toString())+", ")+
-            JavaStringEscapes.wrapJavaString(componentId)+
+            (componentId != null ? JavaStringEscapes.wrapJavaString(componentId) : componentIdSupplier)+
             ")";
     }
 

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/62a295cc/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
----------------------------------------------------------------------
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
index 16ff411..91da883 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/DslAndRebindYamlTest.java
@@ -45,7 +45,9 @@ import org.apache.brooklyn.core.mgmt.persist.BrooklynPersistenceUtils;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.test.Asserts;
 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;
@@ -432,6 +434,78 @@ public class DslAndRebindYamlTest extends AbstractYamlRebindTest {
         Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "hello world");
     }
 
+    @Test
+    public void testDslEntityById() throws Exception {
+        Entity testEntity = setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confObject: $brooklyn:entity(\"x\")");
+        Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_OBJECT), testEntity);
+        
+        Entity e2 = rebind(testEntity);
+        Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_OBJECT), e2);
+    }
+
+    @Test
+    public void testDslEntityWhereIdRetrievedFromAttributeWhenReadyDsl() throws Exception {
+        Entity testEntity = setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confObject: $brooklyn:entity(attributeWhenReady(\"mySensor\"))");
+        testEntity.sensors().set(Sensors.newStringSensor("mySensor"), "x");
+        Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_OBJECT), testEntity);
+        
+        Entity e2 = rebind(testEntity);
+        Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_OBJECT), e2);
+    }
+
+    @Test
+    public void testDslEntityWhereAttributeWhenReadyDslReturnsEntity() throws Exception {
+        Entity testEntity = setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confObject: $brooklyn:entity(attributeWhenReady(\"mySensor\"))");
+        testEntity.sensors().set(Sensors.newSensor(Entity.class, "mySensor"), testEntity);
+        Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_OBJECT), testEntity);
+        
+        Entity e2 = rebind(testEntity);
+        Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_OBJECT), e2);
+    }
+
+    @Test
+    public void testDslChildWhereIdRetrievedFromAttributeWhenReadyDsl() throws Exception {
+        Entity testEntity = setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confObject: $brooklyn:child(attributeWhenReady(\"mySensor\"))",
+                "  brooklyn.children:",
+                "  - type: " + TestEntity.class.getName(),
+                "    id: x");
+        Entity childEntity = Iterables.getOnlyElement(testEntity.getChildren());
+        testEntity.sensors().set(Sensors.newStringSensor("mySensor"), "x");
+        Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_OBJECT), childEntity);
+        
+        Entity e2 = rebind(testEntity);
+        Entity child2 = Iterables.getOnlyElement(e2.getChildren());
+        Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_OBJECT), child2);
+    }
+
+    @Test
+    public void testDslChildWhereAttributeWhenReadyDslReturnsEntityOutOfScopeFails() throws Exception {
+        Entity testEntity = setupAndCheckTestEntityInBasicYamlWith(
+                "  id: x",
+                "  brooklyn.config:",
+                "    test.confObject: $brooklyn:child(attributeWhenReady(\"mySensor\"))");
+        testEntity.sensors().set(Sensors.newSensor(Entity.class, "mySensor"), testEntity);
+        try {
+            Object val = getConfigInTask(testEntity, TestEntity.CONF_OBJECT);
+            Asserts.shouldHaveFailedPreviously("actual="+val);
+        } catch (Exception e) {
+            IllegalStateException ise = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (ise == null || !ise.toString().contains("is not in scope 'child'")) throw e;
+        }
+    }
+
     /*
         - type: org.apache.brooklyn.enricher.stock.Transformer
           brooklyn.config:

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/62a295cc/test-framework/src/test/java/org/apache/brooklyn/test/framework/yaml/TestHttpCallYamlTest.java
----------------------------------------------------------------------
diff --git a/test-framework/src/test/java/org/apache/brooklyn/test/framework/yaml/TestHttpCallYamlTest.java b/test-framework/src/test/java/org/apache/brooklyn/test/framework/yaml/TestHttpCallYamlTest.java
index 38a6328..671b2c4 100644
--- a/test-framework/src/test/java/org/apache/brooklyn/test/framework/yaml/TestHttpCallYamlTest.java
+++ b/test-framework/src/test/java/org/apache/brooklyn/test/framework/yaml/TestHttpCallYamlTest.java
@@ -90,4 +90,25 @@ public class TestHttpCallYamlTest extends AbstractYamlRebindTest {
                 "      equals: 200"
                 );
     }
+    
+    @Test
+    public void testUrlConstructedFromTargetEntity() throws Exception {
+        origApp = (BasicApplication) createStartWaitAndLogApplication(
+                "services:",
+                "- type: " + TestEntity.class.getName(),
+                "  id: target-app",
+                "  brooklyn.config:",
+                "    main.uri: " + server.getUrl(),
+                "- type: " + TestHttpCall.class.getName(),
+                "  brooklyn.config:",
+                "    targetId: target-app",
+                "    url:",
+                "      $brooklyn:formatString:",
+                "      - \"%s/index.html\"",
+                "      - $brooklyn:entity(config(\"targetId\")).config(\"main.uri\")",
+                "    applyAssertionTo: status",
+                "    assert:",
+                "      equals: 200"
+                );
+    }
 }