This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 33a68bd303689f99dd4fd7e29c030aeb8ecf5d97
Author: Alex Heneveld <[email protected]>
AuthorDate: Wed Jul 17 14:59:52 2024 +0100

    add support for dsl to take maps as function args, and to chain when 
necessary
---
 .../spi/dsl/BrooklynDslDeferredSupplier.java       |  10 ++
 .../brooklyn/spi/dsl/DslDeferredFunctionCall.java  |  13 +-
 .../spi/dsl/DslDeferredPropertyAccess.java         |  13 +-
 .../spi/dsl/methods/BrooklynDslCommon.java         | 133 +++++++++++++--------
 .../brooklyn/spi/dsl/methods/DslComponent.java     |  81 +++++++------
 .../spi/dsl/methods/DslToStringHelpers.java        | 123 +++++++++++++++----
 .../brooklyn/camp/brooklyn/ConfigYamlTest.java     |  55 +++++++++
 .../core/entity/lifecycle/ServiceStateLogic.java   |   2 +-
 8 files changed, 315 insertions(+), 115 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 04eee1cb34..971e571c09 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
@@ -161,4 +161,14 @@ public abstract class BrooklynDslDeferredSupplier<T> 
implements DeferredSupplier
         
         Tasks.addTagDynamically(tag);
     }
+
+    public String toString() {
+        try {
+            return toDslString(false);
+        } catch (DslToStringHelpers.YamlSyntaxRequired e) {
+            return toDslString(true);
+        }
+    }
+
+    public abstract String toDslString(boolean yamlAllowed);
 }
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 5d90cb63b7..9c24474805 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
@@ -28,6 +28,7 @@ import org.apache.brooklyn.api.mgmt.Task;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.BrooklynDslCommon;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslToStringHelpers;
 import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.core.task.ImmediateSupplier;
 import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.core.task.ValueResolver;
@@ -170,7 +171,13 @@ public class DslDeferredFunctionCall extends 
BrooklynDslDeferredSupplier<Object>
             instanceArgs = 
ImmutableList.builder().add(obj).addAll(args).build();
             method = Reflections.getMethodFromArgs(instance, fnName, 
instanceArgs);
             if (method.isPresent()) return ;
-    
+
+            method = Reflections.findMethodMaybe((Class)instance, fnName, 
List.class);
+            if (method.isPresent()) {
+                instanceArgs = ImmutableList.of(args);
+                return;
+            }
+
             Maybe<?> facade;
             try {
                 facade = 
Reflections.invokeMethodFromArgs(BrooklynDslCommon.DslFacades.class, "wrap", 
ImmutableList.of(obj));
@@ -239,11 +246,11 @@ public class DslDeferredFunctionCall extends 
BrooklynDslDeferredSupplier<Object>
     }
 
     @Override
-    public String toString() {
+    public String toDslString(boolean yamlAllowed) {
         // prefer the dsl set on us, if set
         if (dsl instanceof String && Strings.isNonBlank((String)dsl)) return 
(String)dsl;
 
-        return DslToStringHelpers.fn(DslToStringHelpers.internal(object) + "." 
+ fnName, args);
+        return DslToStringHelpers.chainFunction(yamlAllowed, object, fnName, 
args);
     }
 
 }
diff --git 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredPropertyAccess.java
 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredPropertyAccess.java
index f5f9a17719..5cbcdd8f66 100644
--- 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredPropertyAccess.java
+++ 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/DslDeferredPropertyAccess.java
@@ -21,6 +21,7 @@ package org.apache.brooklyn.camp.brooklyn.spi.dsl;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.apache.brooklyn.api.mgmt.Task;
 import org.apache.brooklyn.api.objs.Configurable;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslToStringHelpers;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.PropertyAccess;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
@@ -142,11 +143,15 @@ public class DslDeferredPropertyAccess extends 
BrooklynDslDeferredSupplier {
     }
 
     @Override
-    public String toString() {
+    public String toDslString(boolean yamlAllowed) {
         // prefer the dsl set on us, if set
         if (dsl instanceof String && Strings.isNonBlank((String)dsl)) return 
(String)dsl;
-
-        return target + "[" +
-                (index instanceof Integer ? index : 
StringEscapes.JavaStringEscapes.wrapJavaString("" + index)) + "]";
+        String indexS = "[" + (index instanceof Integer ? index : 
StringEscapes.JavaStringEscapes.wrapJavaString("" + index)) + "]";
+        try {
+            return (target==null ? "null" : target.toDslString(false)) + 
indexS;
+        } catch (DslToStringHelpers.YamlSyntaxRequired e) {
+            if (!yamlAllowed) throw e;
+            return "{ $brooklyn:chain: [ "+target.toDslString(true)+", 
"+indexS+" ] }";
+        }
     }
 }
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 6dde3ff5b3..b22207cb67 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,34 +19,28 @@
 package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
-import java.util.*;
-
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.reflect.TypeToken;
-import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec;
-import org.apache.brooklyn.api.mgmt.ManagementContext;
-import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
-import org.apache.brooklyn.api.typereg.RegisteredType;
-import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils;
-import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.function.BiFunction;
-import java.util.function.Supplier;
-
+import com.thoughtworks.xstream.annotations.XStreamConverter;
 import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.mgmt.ExecutionContext;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
 import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
 import org.apache.brooklyn.api.objs.Configurable;
 import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.api.typereg.RegisteredType;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampReservedKeys;
 import 
org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynYamlTypeInstantiator;
 import org.apache.brooklyn.camp.brooklyn.spi.creation.EntitySpecConfiguration;
-import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
-import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.*;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.parse.WorkflowTransformGet;
 import org.apache.brooklyn.config.ConfigKey;
@@ -55,7 +49,6 @@ import 
org.apache.brooklyn.core.config.external.ExternalConfigSupplier;
 import org.apache.brooklyn.core.entity.EntityDynamicType;
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
-import 
org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext;
 import org.apache.brooklyn.core.mgmt.internal.ExternalConfigSupplierRegistry;
 import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
 import org.apache.brooklyn.core.mgmt.persist.DeserializingClassRenamesProvider;
@@ -81,12 +74,10 @@ import org.apache.brooklyn.util.core.task.Tasks;
 import 
org.apache.brooklyn.util.core.xstream.ObjectWithDefaultStringImplConverter;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
-import org.apache.brooklyn.util.guava.TypeTokens;
 import org.apache.brooklyn.util.javalang.Reflections;
 import org.apache.brooklyn.util.javalang.coerce.TryCoercer;
 import org.apache.brooklyn.util.javalang.coerce.TypeCoercer;
 import org.apache.brooklyn.util.net.Urls;
-import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.yaml.Yamls;
@@ -94,13 +85,15 @@ import org.apache.commons.beanutils.BeanUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.base.Function;
-import com.google.common.base.Objects;
-import com.google.common.base.Optional;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.thoughtworks.xstream.annotations.XStreamConverter;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+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;
 
 /**
  * static import functions which can be used in `$brooklyn:xxx` contexts
@@ -327,14 +320,18 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.concat(DslToStringHelpers.internal(obj), 
".", DslToStringHelpers.fn("config", keyName));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunction(yamlAllowed, obj, 
"config", Collections.singletonList(keyName));
         }
     }
 
     @DslAccessible
     public static BrooklynDslDeferredSupplier<?> attributeWhenReady(Object 
sensorName) {
-        return new DslComponent(Scope.THIS, "").attributeWhenReady(sensorName);
+        return attributeWhenReady(sensorName, null);
+    }
+    @DslAccessible
+    public static BrooklynDslDeferredSupplier<?> attributeWhenReady(final 
Object sensorName, Map options) {
+        return new DslComponent(Scope.THIS, "").attributeWhenReady(sensorName, 
options);
     }
 
     @DslAccessible
@@ -503,10 +500,8 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return "$brooklyn:literal(" +
-                    (literalString!=null ? 
JavaStringEscapes.wrapJavaString(literalString) : literalObjectJson)
-                    +")";
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.fn1(yamlAllowed, "literal", 
literalString!=null ? literalString : literalObjectJson);
         }
     }
 
@@ -593,8 +588,8 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return "$brooklyn:urlEncode("+arg+")";
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.fn1(yamlAllowed, "urlEncode", arg);
         }
     }
 
@@ -648,8 +643,8 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.fn("formatString", 
MutableList.<Object>builder().add(pattern).addAll(args).build());
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.fn(yamlAllowed, "formatString", 
MutableList.<Object>builder().add(pattern).addAll(args).build());
         }
     }
 
@@ -705,8 +700,8 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.fn("regexReplace", source, pattern, 
replacement);
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.fn(yamlAllowed, "regexReplacement", 
source, pattern, replacement);
         }
     }
 
@@ -950,15 +945,24 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
+        public String toDslString(boolean yamlAllowed) {
             // prefer the dsl set on us, if set
             if (dsl instanceof String && Strings.isNonBlank((String)dsl)) 
return (String)dsl;
 
-            Object arg = type != null ? type.getName() : typeName;
+            String arg = type != null ? type.getName() : typeName;
+            // old approach
+//            if (!constructorArgs.isEmpty()) {
+//                arg = 
MutableList.<Object>of(arg).appendAll(constructorArgs).toString();
+//            }
+//            return DslToStringHelpers.fn1(yamlAllowed, "object", arg);
+
+            // new approach; preserve list
             if (!constructorArgs.isEmpty()) {
-                arg = 
MutableList.of(arg).appendAll(constructorArgs).toString();
+                return DslToStringHelpers.fn(yamlAllowed, "object", 
MutableList.<Object>of(arg).appendAll(constructorArgs));
+            } else {
+                return DslToStringHelpers.fn1(yamlAllowed, "object", arg);
             }
-            return DslToStringHelpers.fn("object", arg);
+
         }
     }
 
@@ -1026,19 +1030,48 @@ public class BrooklynDslCommon {
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.fn("external", providerName, key);
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.fn(yamlAllowed, "external", 
providerName, key);
         }
     }
 
+    @DslAccessible
     public static Object template(Object template) {
         return new DslComponent(Scope.THIS, "").template(template);
     }
 
+    @DslAccessible
     public static Object template(Object template, Map<? ,?> substitutions) {
         return new DslComponent(Scope.THIS, "").template(template, 
substitutions);
     }
 
+    @DslAccessible
+    public static Object chain(List<Object> parts) {
+        Iterator<Object> pi = parts.iterator();
+        if (!pi.hasNext()) throw new IllegalArgumentException("chain must take 
at least one argument");
+        Object resultO = pi.next();
+        if (!(resultO instanceof BrooklynDslDeferredSupplier)) throw new 
IllegalStateException("Invalid argument to chain. First argument should be a 
DSL supplier.");
+
+        BrooklynDslDeferredSupplier result = (BrooklynDslDeferredSupplier) 
resultO;
+        while (pi.hasNext()) {
+            Object next = pi.next();
+            if (next instanceof String && ((String) next).startsWith("[")) {
+                String ns = ((String) next).substring(1).trim();
+                if (!ns.endsWith("]")) throw new 
IllegalArgumentException("Property access must be wrapped in open and close 
square bracket");
+                Object index;
+                if (ns.startsWith("\"")) index = 
JavaStringEscapes.unwrapJavaString(ns);
+                else index = Integer.parseInt(ns);
+                // dynamic indexes are not supported, neither here nor in the 
class below
+                result = new DslDeferredPropertyAccess(result, next);
+
+            } else if (next instanceof Map && ((Map)next).size()==1) {
+                Map nm = (Map) next;
+                result = new DslDeferredFunctionCall(result, (String) 
Iterables.getOnlyElement(nm.keySet()), (List) 
Iterables.getOnlyElement(nm.values()));
+            }
+        }
+        return result;
+    }
+
     public static class Functions {
         @DslAccessible
         public static Object regexReplacement(final Object pattern, final 
Object replacement) {
@@ -1094,8 +1127,8 @@ public class BrooklynDslCommon {
             }
 
             @Override
-            public String toString() {
-                return DslToStringHelpers.fn("function.regexReplace", pattern, 
replacement);
+            public String toDslString(boolean yamlAllowed) {
+                return DslToStringHelpers.fn(yamlAllowed, 
"function.regexReplace", pattern, replacement);
             }
         }
     }
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 6838013374..cb0e14c382 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
@@ -19,24 +19,13 @@
 package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.google.common.base.CaseFormat;
-import com.google.common.base.Converter;
 import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
+import com.google.common.base.*;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Callables;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
-
-import java.util.*;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.Group;
 import org.apache.brooklyn.api.location.Location;
@@ -46,13 +35,9 @@ import org.apache.brooklyn.api.objs.BrooklynObject;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
 import org.apache.brooklyn.api.sensor.Sensor;
 import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
-import org.apache.brooklyn.core.mgmt.internal.AppGroupTraverser;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslAccessible;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.DslFunctionSource;
-
-import static com.jayway.jsonpath.Filter.filter;
-import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.entity.Entities;
@@ -60,7 +45,7 @@ import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.EntityPredicates;
 import org.apache.brooklyn.core.location.Locations;
 import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
-import org.apache.brooklyn.core.mgmt.internal.LocalEntityManager;
+import org.apache.brooklyn.core.mgmt.internal.AppGroupTraverser;
 import org.apache.brooklyn.core.sensor.DependentConfiguration;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.util.JavaGroovyEquivalents;
@@ -79,6 +64,14 @@ import org.apache.brooklyn.util.text.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.apache.brooklyn.camp.brooklyn.spi.dsl.DslUtils.resolved;
+
 public class DslComponent extends BrooklynDslDeferredSupplier<Entity> 
implements DslFunctionSource {
 
     private static final Logger log = 
LoggerFactory.getLogger(DslComponent.class);
@@ -654,8 +647,8 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
             return Objects.equal(this.component, that.component);
         }
         @Override
-        public String toString() {
-            return DslToStringHelpers.component(component, 
DslToStringHelpers.fn("entityId"));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent(yamlAllowed, 
component, "entityId");
         }
     }
 
@@ -663,20 +656,30 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
     public BrooklynDslDeferredSupplier<?> attributeWhenReady(final Object 
sensorNameOrSupplier) {
         return new AttributeWhenReady(this, sensorNameOrSupplier);
     }
+    @DslAccessible
+    public BrooklynDslDeferredSupplier<?> attributeWhenReady(final Object 
sensorNameOrSupplier, Map options) {
+        return new AttributeWhenReady(this, sensorNameOrSupplier, options);
+    }
     public static class AttributeWhenReady extends 
BrooklynDslDeferredSupplier<Object> {
         private static final long serialVersionUID = 1740899524088902383L;
         private final DslComponent component;
         @XStreamConverter(ObjectWithDefaultStringImplConverter.class)
         private final Object sensorName;
+        private final Map options;
 
         // JSON deserialization only
         private AttributeWhenReady() {
             this.component = null;
             this.sensorName = null;
+            this.options = null;
         }
         public AttributeWhenReady(DslComponent component, Object sensorName) {
+            this(component, sensorName, null);
+        }
+        public AttributeWhenReady(DslComponent component, Object sensorName, 
Map opts) {
             this.component = Preconditions.checkNotNull(component);
             this.sensorName = sensorName;
+            this.options = opts;
         }
 
         public Object getSensorName() {
@@ -726,7 +729,8 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
             if (!(targetSensor instanceof AttributeSensor<?>)) {
                 targetSensor = Sensors.newSensor(Object.class, sensorNameS);
             }
-            return (Task<Object>) 
DependentConfiguration.attributeWhenReady(targetEntity, 
(AttributeSensor<?>)targetSensor);
+            return (Task<Object>) 
DependentConfiguration.attributeWhenReady(targetEntity, 
(AttributeSensor<?>)targetSensor, options!=null ?
+                    TypeCoercions.coerce(options, 
DependentConfiguration.AttributeWhenReadyOptions.class) : 
DependentConfiguration.AttributeWhenReadyOptions.defaultOptions());
         }
 
         @Override
@@ -742,8 +746,9 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                     Objects.equal(this.sensorName, that.sensorName);
         }
         @Override
-        public String toString() {
-            return DslToStringHelpers.component(component, 
DslToStringHelpers.fn("attributeWhenReady", sensorName));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent(yamlAllowed, 
component,
+                    "attributeWhenReady", 
MutableList.of(sensorName).appendIfNotNull(options));
         }
     }
 
@@ -850,8 +855,8 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.component(component, 
DslToStringHelpers.fn("config", keyName));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent1(yamlAllowed, 
component, "config", keyName);
         }
     }
 
@@ -958,9 +963,9 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
         }
 
         @Override
-        public String toString() {
-            return DslToStringHelpers.component(component, 
DslToStringHelpers.fn("sensorName", 
-                sensorName instanceof Sensor ? 
((Sensor<?>)sensorName).getName() : sensorName));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent1(yamlAllowed, 
component, "sensorName",
+                sensorName instanceof Sensor ? 
((Sensor<?>)sensorName).getName() : sensorName);
         }
     }
 
@@ -1092,8 +1097,8 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                     Objects.equal(this.index, that.index);
         }
         @Override
-        public String toString() {
-            return DslToStringHelpers.component(component, 
DslToStringHelpers.fn("location", index));
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent1(yamlAllowed, 
component, "location", index);
         }
     }
 
@@ -1183,6 +1188,11 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
                 }
             }).build();
         }
+
+        @Override
+        public String toDslString(boolean yamlAllowed) {
+            return DslToStringHelpers.chainFunctionOnComponent(yamlAllowed, 
component, "template", MutableList.of(template).appendIfNotNull(substitutions));
+        }
     }
 
     public static enum Scope {
@@ -1244,28 +1254,27 @@ public class DslComponent extends 
BrooklynDslDeferredSupplier<Entity> implements
     }
 
     @Override
-    public String toString() {
+    public String toDslString(boolean yamlAllowed) {
         Object component = componentId != null ? componentId : 
componentIdSupplier;
         
         if (scope==Scope.GLOBAL) {
-            return DslToStringHelpers.fn("entity", component);
+            return DslToStringHelpers.fn1(yamlAllowed, "entity", component);
         }
         
         if (scope==Scope.THIS) {
             if (scopeComponent!=null) {
                 return scopeComponent.toString();
             }
-            return DslToStringHelpers.fn("entity", "this", "");
+            return DslToStringHelpers.fn(yamlAllowed, "entity", "this", "");
         }
         
         String remainder;
         if (component==null || "".equals(component)) {
-            remainder = DslToStringHelpers.fn(scope.toString());
+            return DslToStringHelpers.chainFunctionOnComponent(yamlAllowed, 
scopeComponent, scope.toString());
         } else {
-            remainder = DslToStringHelpers.fn(scope.toString(), component);
+            return DslToStringHelpers.chainFunctionOnComponent1(yamlAllowed, 
scopeComponent, scope.toString(), component);
         }
-               
-        return DslToStringHelpers.component(scopeComponent, remainder);
+
     }
     
 }
diff --git 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslToStringHelpers.java
 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslToStringHelpers.java
index 8bf4460f98..4dd9cfb179 100644
--- 
a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslToStringHelpers.java
+++ 
b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/dsl/methods/DslToStringHelpers.java
@@ -19,8 +19,13 @@
 package org.apache.brooklyn.camp.brooklyn.spi.dsl.methods;
 
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.apache.brooklyn.api.objs.BrooklynObject;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier;
 import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.text.Strings;
@@ -32,6 +37,7 @@ public class DslToStringHelpers {
     
     /** concatenates the arguments, each appropriately dsl-tostringed with 
prefixes removed,
      * and then prefixes the result */
+    // no longer needed now that we use chain... functions
     public static String concat(String ...x) {
         StringBuilder r = new StringBuilder();
         for (String xi: x) {
@@ -44,36 +50,111 @@ public class DslToStringHelpers {
     }
 
     /** convenience for functions, inserting parentheses and commas */
-    public static String fn(String functionName, Iterable<?> args) {
-        StringBuilder out = new StringBuilder();
-        out.append(BrooklynDslCommon.PREFIX);
-        out.append(functionName);
-        out.append("(");
-        if (args!=null) {
-            boolean nonFirst = false;
-            for (Object s: args) {
-                if (nonFirst) out.append(", ");
-                out.append(internal(s));
-                nonFirst = true;
+    public static String fn(boolean yamlAllowed, String functionName, 
Iterable<?> args) {
+        return fn(yamlAllowed, true, functionName, args);
+    }
+    public static String fn(boolean yamlAllowed, boolean prefixWanted, String 
functionName, Iterable<?> args) {
+        try {
+            StringBuilder out = new StringBuilder();
+            if (prefixWanted) out.append(BrooklynDslCommon.PREFIX);
+            out.append(functionName);
+            out.append("(");
+            if (args != null) {
+                boolean nonFirst = false;
+                for (Object s : args) {
+                    if (nonFirst) out.append(", ");
+                    out.append(internal(false, s));
+                    nonFirst = true;
+                }
+            }
+            out.append(")");
+            return out.toString();
+        } catch (YamlSyntaxRequired e) {
+            if (!yamlAllowed) throw e;
+
+            StringBuilder out = new StringBuilder();
+            out.append("{ ");
+            if (prefixWanted) out.append(BrooklynDslCommon.PREFIX);
+            out.append(functionName);
+            out.append(": [ ");
+            if (args != null) {
+                boolean nonFirst = false;
+                for (Object s : args) {
+                    if (nonFirst) out.append(", ");
+                    out.append(internal(true, s));
+                    nonFirst = true;
+                }
             }
+            out.append("] }");
+            return out.toString();
         }
-        out.append(")");
-        return out.toString();
     }
 
-    public static String fn(String functionName, Object ...args) {
-        return fn(functionName, Arrays.asList(args));
+    public static String fn(boolean yamlAllowed, String functionName) {
+        return fn(yamlAllowed, functionName, Collections.emptyList());
     }
-    
+    public static String fn1(boolean yamlAllowed, String functionName, Object 
arg1) {
+        return fn(yamlAllowed, functionName, Collections.singletonList(arg1));
+    }
+    public static String fn(boolean yamlAllowed, String functionName, Object 
arg1, Object arg2, Object ...args) {
+        return fn(yamlAllowed, functionName, MutableList.of(arg1, 
arg2).appendAll(Arrays.asList(args)));
+    }
+
+    public static class YamlSyntaxRequired extends RuntimeException {}
+
     /** convenience for internal arguments, removing any prefix, and applying 
wrap/escapes and other conversions */
-    public static String internal(Object x) {
+    public static String internal(boolean yamlAllowed, Object x) {
+        return internal(yamlAllowed, false, x);
+    }
+    public static String internal(boolean yamlAllowed, boolean prefixWanted, 
Object x) {
         if (x==null) return "null";
         if (x instanceof String) return 
JavaStringEscapes.wrapJavaString((String)x);
-        if (x instanceof BrooklynObject) return fn("entity", MutableList.of( 
((BrooklynObject)x).getId() ));
-        return Strings.removeFromStart(x.toString(), BrooklynDslCommon.PREFIX);
+        if (x instanceof BrooklynObject) return fn(yamlAllowed, "entity", 
MutableList.of( ((BrooklynObject)x).getId() ));
+
+        // these may not produce valid yaml, if used from within a function
+        if (!yamlAllowed) {
+            if (x instanceof Iterable || x instanceof Map) throw new 
YamlSyntaxRequired();
+        }
+        if (x instanceof Iterable) return "[ " + 
MutableList.copyOf((Iterable)x).stream().map(xi -> internal(yamlAllowed, true, 
xi)).collect(Collectors.joining(", ")) + " ]";
+        if (x instanceof Map) return "{ " + 
((Map<?,?>)x).entrySet().stream().map(entry ->
+                DslToStringHelpers.internal(yamlAllowed, entry.getKey())+": 
"+DslToStringHelpers.internal(yamlAllowed, true, 
entry.getValue())).collect(Collectors.joining(", ")) + " }";
+
+        String v;
+        if (x instanceof BrooklynDslDeferredSupplier) {
+            v = ((BrooklynDslDeferredSupplier<?>) x).toDslString(yamlAllowed);
+        } else {
+            v = x.toString();
+        }
+        if (!prefixWanted) v = Strings.removeFromStart(v, 
BrooklynDslCommon.PREFIX);
+        return v;
+    }
+
+    public static String chainFunctionOnComponent(boolean yamlAllowed, 
DslComponent component, String fnNameNoArgs) {
+        return chainFunctionOnComponent(yamlAllowed, component, fnNameNoArgs, 
Collections.emptyList());
+    }
+    public static String chainFunctionOnComponent(boolean yamlAllowed, 
DslComponent component, String fnName, Object arg1, Object arg2, Object 
...args) {
+        return chainFunctionOnComponent(yamlAllowed, component, fnName, 
MutableList.of(arg1, arg2).appendAll(Arrays.asList(args)));
+    }
+    public static String chainFunctionOnComponent1(boolean yamlAllowed, 
DslComponent component, String fnName, Object arg1) {
+        return chainFunctionOnComponent(yamlAllowed, component, fnName, 
Collections.singletonList(arg1));
+    }
+    public static String chainFunctionOnComponent(boolean yamlAllowed, 
DslComponent component, String fnName, List<Object> args) {
+        if (component==null || component.getScope()==Scope.THIS) {
+            // ignore component
+            return DslToStringHelpers.fn(yamlAllowed, fnName, args);
+        }
+        return chainFunction(yamlAllowed, component, fnName, args);
     }
 
-    public static String component(DslComponent component, String remainder) {
-        return component==null || component.getScope()==Scope.THIS ? remainder 
: concat(internal(component), ".", remainder);
+    public static String chainFunction(boolean yamlAllowed, Object object, 
String fnName, List<?> args) {
+        try {
+            return DslToStringHelpers.fn(false, 
DslToStringHelpers.internal(false, object) + "." + fnName, args);
+        } catch (YamlSyntaxRequired e) {
+            if (!yamlAllowed) throw e;
+            return "{ $brooklyn:chain: [ "+
+                    DslToStringHelpers.internal(true, true, object) + ", " +
+                    "{ "+fnName+": "+internal(true, args) + " } ] }";
+        }
     }
+
 }
diff --git 
a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigYamlTest.java
 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigYamlTest.java
index fcbccfdacd..8b819ba880 100644
--- 
a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigYamlTest.java
+++ 
b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/ConfigYamlTest.java
@@ -20,9 +20,13 @@ package org.apache.brooklyn.camp.brooklyn;
 
 import com.google.common.annotations.Beta;
 import java.util.Map;
+
+import com.google.common.base.Stopwatch;
 import org.apache.brooklyn.core.config.Sanitizer;
+import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.internal.BrooklynProperties;
 import org.apache.brooklyn.core.server.BrooklynServerConfig;
+import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.internal.BrooklynSystemProperties;
 import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
 import org.apache.brooklyn.util.yaml.Yamls;
@@ -431,6 +435,57 @@ public class ConfigYamlTest extends AbstractYamlTest {
         
assertEquals(entity.config().getNonBlocking(TestEntity.CONF_SET_PLAIN).get(), 
ImmutableSet.of("myOther"));
     }
 
+    @Test
+    public void testAttributeWhenReadyOptions() throws Exception {
+        String yaml = Joiner.on("\n").join(
+                "services:",
+                "- type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "  brooklyn.config:",
+                "    test.confName: { $brooklyn:attributeWhenReady: [ 
\"test.name\", { timeout: 10ms } ] }");
+
+        final Entity app = createStartWaitAndLogApplication(yaml);
+        final TestEntity entity = (TestEntity) 
Iterables.getOnlyElement(app.getChildren());
+
+        // Attribute not yet set; non-blocking will return promptly without 
the value
+        Stopwatch sw = Stopwatch.createStarted();
+        Asserts.assertFailsWith(() -> 
entity.config().get(TestEntity.CONF_NAME),
+                Asserts.expectedFailureContainsIgnoreCase("Cannot resolve", 
"$brooklyn:attributeWhenReady", "test.name", "10ms", "Resolving config 
test.confName", "Unsatisfied after "));
+        Asserts.assertThat(Duration.of(sw.elapsed()), d -> 
d.isLongerThan(Duration.millis(9)));
+
+        entity.sensors().set(TestEntity.NAME, "x");
+        EntityAsserts.assertConfigEquals(entity, TestEntity.CONF_NAME, "x");
+    }
+
+    @Test
+    public void testOtherEntityAttributeWhenReadyOptions() throws Exception {
+        String v0 = "{ $brooklyn:chain: [ $brooklyn:entity(\"entity2\"), { 
attributeWhenReady: [ \"test.name\", { timeout: 10ms } ] } ] }";
+        String v1 = "{ $brooklyn:chain: [ $brooklyn:entity(\"entity2\"), { 
attributeWhenReady: [ \"test.name\", { \"timeout\": \"10ms\" } ] } ] }";
+
+        String yaml = Joiner.on("\n").join(
+                "services:",
+                "- type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "  brooklyn.config:",
+                "    test.confName: "+v0,
+                "- type: org.apache.brooklyn.core.test.entity.TestEntity",
+                "  id: entity2");
+
+        final Entity app = createStartWaitAndLogApplication(yaml);
+        final TestEntity entity1 = (TestEntity) 
Iterables.get(app.getChildren(), 0);
+        final TestEntity entity2 = (TestEntity) 
Iterables.get(app.getChildren(), 1);
+
+        // Attribute not yet set; non-blocking will return promptly without 
the value
+        Stopwatch sw = Stopwatch.createStarted();
+        Asserts.assertFailsWith(() -> 
entity1.config().get(TestEntity.CONF_NAME),
+                Asserts.expectedFailureContainsIgnoreCase("Cannot resolve", 
"$brooklyn:chain", " attributeWhenReady", "test.name", "10ms", "Resolving 
config test.confName", "Unsatisfied after "));
+        Asserts.assertThat(Duration.of(sw.elapsed()), d -> 
d.isLongerThan(Duration.millis(9)));
+
+        entity2.sensors().set(TestEntity.NAME, "x");
+        EntityAsserts.assertConfigEquals(entity1, TestEntity.CONF_NAME, "x");
+
+        Maybe<Object> rawV = entity1.config().getRaw(TestEntity.CONF_NAME);
+        Asserts.assertEquals(rawV.get().toString(), v1);
+    }
+
     @Test
     public void testConfigGoodNumericCoercions() throws Exception {
         String yaml = Joiner.on("\n").join(
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
index 5ded2a7033..b294c0d6b7 100644
--- 
a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
@@ -181,7 +181,7 @@ public class ServiceStateLogic {
         return expected.getState();
     }
 
-    private static void waitBrieflyForServiceUpIfStateIsRunning(String when, 
Entity entity, Lifecycle state) {
+    public static void waitBrieflyForServiceUpIfStateIsRunning(String when, 
Entity entity, Lifecycle state) {
         if (state==Lifecycle.RUNNING) {
             Boolean up = entity.getAttribute(Attributes.SERVICE_UP);
             if (!Boolean.TRUE.equals(up) && Entities.isManagedActive(entity)) {


Reply via email to