You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/04/17 16:34:59 UTC

[3/8] incubator-brooklyn git commit: make enrichers easier to configure from yaml

make enrichers easier to configure from yaml

* entity spec keeps the list of specs, for things like enrichers, because equality (set duplication) is not very good for specs
* makes many of the basic enrichers easier to configure from yaml, with more flexible config
* in particular `Transformer` can be given a value supplier, e.g. `$brooklyn:formatString`
* adds a `Joiner` enricher which does `Strings.join`, handy for converting a list to something which can be used in bash
* good example of all of these in test-app-with-enrichers-slightly-simpler.yaml, referenced in the docs reference page


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

Branch: refs/heads/master
Commit: f7142a3333fdabdbec0e6eb606e7b595fd8491ef
Parents: 18b6529
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Sun Apr 12 19:56:16 2015 -0500
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Sun Apr 12 20:00:53 2015 -0500

----------------------------------------------------------------------
 .../brooklyn/entity/proxying/EntitySpec.java    |  11 +-
 .../main/java/brooklyn/enricher/Enrichers.java  |  83 +++++++++++-
 .../enricher/basic/AbstractAggregator.java      |   7 +-
 .../brooklyn/enricher/basic/Aggregator.java     |  17 ++-
 .../java/brooklyn/enricher/basic/Joiner.java    | 128 +++++++++++++++++++
 .../brooklyn/enricher/basic/Propagator.java     |  51 +++++---
 .../brooklyn/enricher/basic/Transformer.java    |  81 +++++++++---
 .../java/brooklyn/enricher/EnrichersTest.java   |  45 +++++++
 ...est-app-with-enrichers-slightly-simpler.yaml |  57 +++++++++
 docs/guide/yaml/yaml-reference.md               |   5 +-
 .../spi/dsl/BrooklynDslInterpreter.java         |   6 +-
 .../spi/dsl/methods/BrooklynDslCommon.java      |   3 +-
 .../EnrichersSlightlySimplerYamlTest.java       |  96 ++++++++++++++
 ...est-app-with-enrichers-slightly-simpler.yaml |  74 +++++++++++
 .../brooklyn/util/text/StringPredicates.java    |  22 ++++
 15 files changed, 636 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/api/src/main/java/brooklyn/entity/proxying/EntitySpec.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/proxying/EntitySpec.java b/api/src/main/java/brooklyn/entity/proxying/EntitySpec.java
index 6fa8e73..4d8a643 100644
--- a/api/src/main/java/brooklyn/entity/proxying/EntitySpec.java
+++ b/api/src/main/java/brooklyn/entity/proxying/EntitySpec.java
@@ -41,6 +41,7 @@ import brooklyn.policy.Enricher;
 import brooklyn.policy.EnricherSpec;
 import brooklyn.policy.Policy;
 import brooklyn.policy.PolicySpec;
+import brooklyn.util.collections.MutableList;
 
 import com.google.common.base.Supplier;
 import com.google.common.base.Throwables;
@@ -428,14 +429,14 @@ public class EntitySpec<T extends Entity> extends AbstractBrooklynObjectSpec<T,E
     /** adds the supplied policies to the spec */
     public <V> EntitySpec<T> policySpecs(Iterable<? extends PolicySpec<?>> val) {
         checkMutable();
-        policySpecs.addAll(Sets.newLinkedHashSet(checkNotNull(val, "policySpecs")));
+        policySpecs.addAll(MutableList.copyOf(checkNotNull(val, "policySpecs")));
         return this;
     }
     
     /** adds the supplied policies to the spec */
     public <V> EntitySpec<T> policies(Iterable<? extends Policy> val) {
         checkMutable();
-        policies.addAll(Sets.newLinkedHashSet(checkNotNull(val, "policies")));
+        policies.addAll(MutableList.copyOf(checkNotNull(val, "policies")));
         return this;
     }
     
@@ -456,14 +457,14 @@ public class EntitySpec<T extends Entity> extends AbstractBrooklynObjectSpec<T,E
     /** adds the supplied policies to the spec */
     public <V> EntitySpec<T> enricherSpecs(Iterable<? extends EnricherSpec<?>> val) {
         checkMutable();
-        enricherSpecs.addAll(Sets.newLinkedHashSet(checkNotNull(val, "enricherSpecs")));
+        enricherSpecs.addAll(MutableList.copyOf(checkNotNull(val, "enricherSpecs")));
         return this;
     }
     
     /** adds the supplied policies to the spec */
     public <V> EntitySpec<T> enrichers(Iterable<? extends Enricher> val) {
         checkMutable();
-        enrichers.addAll(Sets.newLinkedHashSet(checkNotNull(val, "enrichers")));
+        enrichers.addAll(MutableList.copyOf(checkNotNull(val, "enrichers")));
         return this;
     }
     
@@ -477,7 +478,7 @@ public class EntitySpec<T extends Entity> extends AbstractBrooklynObjectSpec<T,E
     /** adds the supplied locations to the spec */
     public <V> EntitySpec<T> locations(Iterable<? extends Location> val) {
         checkMutable();
-        locations.addAll(Sets.newLinkedHashSet(checkNotNull(val, "locations")));
+        locations.addAll(MutableList.copyOf(checkNotNull(val, "locations")));
         return this;
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/Enrichers.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/Enrichers.java b/core/src/main/java/brooklyn/enricher/Enrichers.java
index a474f37..9b34b13 100644
--- a/core/src/main/java/brooklyn/enricher/Enrichers.java
+++ b/core/src/main/java/brooklyn/enricher/Enrichers.java
@@ -29,6 +29,7 @@ import java.util.Set;
 import brooklyn.enricher.basic.AbstractEnricher;
 import brooklyn.enricher.basic.Aggregator;
 import brooklyn.enricher.basic.Combiner;
+import brooklyn.enricher.basic.Joiner;
 import brooklyn.enricher.basic.Propagator;
 import brooklyn.enricher.basic.Transformer;
 import brooklyn.enricher.basic.UpdatingMap;
@@ -42,13 +43,14 @@ import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.collections.MutableSet;
 import brooklyn.util.flags.TypeCoercions;
+import brooklyn.util.text.StringPredicates;
 import brooklyn.util.text.Strings;
 
 import com.google.common.base.Function;
-import com.google.common.base.Joiner;
 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.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -166,6 +168,12 @@ public class Enrichers {
         public <S,TKey,TVal> UpdatingMapBuilder<S, TKey, TVal> updatingMap(AttributeSensor<Map<TKey,TVal>> target) {
             return new UpdatingMapBuilder<S, TKey, TVal>(target);
         }
+        /** creates a {@link brooklyn.enricher.basic.Joiner} enricher builder
+         * which joins entries in a list to produce a String
+         **/
+        public JoinerBuilder joining(AttributeSensor<?> source) {
+            return new JoinerBuilder(source);
+        }
     }
 
 
@@ -262,6 +270,8 @@ public class Enrichers {
                                 ((input instanceof CharSequence) ? Strings.isNonBlank((CharSequence)input) : true);
                     }
                 };
+                // above kept for deserialization; not sure necessary
+                valueFilter = StringPredicates.isNonBlank(); 
             } else {
                 valueFilter = null;
             }
@@ -496,10 +506,10 @@ public class Enrichers {
             if (propagatingAllBut!=null && !Iterables.isEmpty(propagatingAllBut)) {
                 List<String> allBut = MutableList.of();
                 for (Sensor<?> s: propagatingAllBut) allBut.add(s.getName());
-                summary.add("ALL_BUT:"+Joiner.on(",").join(allBut));
+                summary.add("ALL_BUT:"+com.google.common.base.Joiner.on(",").join(allBut));
             }
             
-            return "propagating["+fromEntity.getId()+":"+Joiner.on(",").join(summary)+"]";
+            return "propagating["+fromEntity.getId()+":"+com.google.common.base.Joiner.on(",").join(summary)+"]";
         }
         public EnricherSpec<? extends Enricher> build() {
             return super.build().configure(MutableMap.builder()
@@ -581,6 +591,67 @@ public class Enrichers {
         }
     }
 
+    protected abstract static class AbstractJoinerBuilder<B extends AbstractJoinerBuilder<B>> extends AbstractEnricherBuilder<B> {
+        protected final AttributeSensor<?> transforming;
+        protected AttributeSensor<String> publishing;
+        protected Entity fromEntity;
+        protected String separator;
+        protected Boolean quote;
+        protected Integer minimum;
+        protected Integer maximum;
+
+        public AbstractJoinerBuilder(AttributeSensor<?> source) {
+            super(Joiner.class);
+            this.transforming = checkNotNull(source);
+        }
+        public B publishing(AttributeSensor<String> target) {
+            this.publishing = checkNotNull(target);
+            return self();
+        }
+        public B separator(String separator) {
+            this.separator = separator;
+            return self();
+        }
+        public B quote(Boolean quote) {
+            this.quote = quote;
+            return self();
+        }
+        public B minimum(Integer minimum) {
+            this.minimum = minimum;
+            return self();
+        }
+        public B maximum(Integer maximum) {
+            this.maximum = maximum;
+            return self();
+        }
+        @Override
+        protected String getDefaultUniqueTag() {
+            if (transforming==null || publishing==null) return null;
+            return "joiner:"+transforming.getName()+"->"+publishing.getName();
+        }
+        public EnricherSpec<?> build() {
+            return super.build().configure(MutableMap.builder()
+                            .putIfNotNull(Joiner.PRODUCER, fromEntity)
+                            .put(Joiner.TARGET_SENSOR, publishing)
+                            .put(Joiner.SOURCE_SENSOR, transforming)
+                            .putIfNotNull(Joiner.SEPARATOR, separator)
+                            .putIfNotNull(Joiner.QUOTE, quote)
+                            .putIfNotNull(Joiner.MINIMUM, minimum)
+                            .putIfNotNull(Joiner.MAXIMUM, maximum)
+                            .build());
+        }
+        
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .omitNullValues()
+                    .add("publishing", publishing)
+                    .add("transforming", transforming)
+                    .add("separator", separator)
+                    .toString();
+        }
+    }
+    
     public static class InitialBuilder extends AbstractInitialBuilder<InitialBuilder> {
     }
 
@@ -626,6 +697,12 @@ public class Enrichers {
         }
     }
 
+    public static class JoinerBuilder extends AbstractJoinerBuilder<JoinerBuilder> {
+        public JoinerBuilder(AttributeSensor<?> source) {
+            super(source);
+        }
+    }
+
     protected static <T extends Number> T average(Collection<T> vals, Number defaultValueForUnreportedSensors, Number valueToReportIfNoSensors, TypeToken<T> type) {
         Double doubleValueToReportIfNoSensors = (valueToReportIfNoSensors == null) ? null : valueToReportIfNoSensors.doubleValue();
         int count = count(vals, defaultValueForUnreportedSensors!=null);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/basic/AbstractAggregator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/AbstractAggregator.java b/core/src/main/java/brooklyn/enricher/basic/AbstractAggregator.java
index 9568332..a76a602 100644
--- a/core/src/main/java/brooklyn/enricher/basic/AbstractAggregator.java
+++ b/core/src/main/java/brooklyn/enricher/basic/AbstractAggregator.java
@@ -117,10 +117,15 @@ public abstract class AbstractAggregator<T,U> extends AbstractEnricher implement
         this.fromMembers = Maybe.fromNullable(getConfig(FROM_MEMBERS)).or(fromMembers);
         this.fromChildren = Maybe.fromNullable(getConfig(FROM_CHILDREN)).or(fromChildren);
         this.entityFilter = (Predicate<? super Entity>) (getConfig(ENTITY_FILTER) == null ? Predicates.alwaysTrue() : getConfig(ENTITY_FILTER));
-        this.valueFilter = (Predicate<? super T>) (getConfig(VALUE_FILTER) == null ? Predicates.alwaysTrue() : getConfig(VALUE_FILTER));
+        this.valueFilter = (Predicate<? super T>) (getConfig(VALUE_FILTER) == null ? getDefaultValueFilter() : getConfig(VALUE_FILTER));
         
         setEntityLoadingTargetConfig();
     }
+    
+    protected Predicate<?> getDefaultValueFilter() {
+        return Predicates.alwaysTrue();
+    }
+
     @SuppressWarnings({ "unchecked" })
     protected void setEntityLoadingTargetConfig() {
         this.targetSensor = (Sensor<U>) getRequiredConfig(TARGET_SENSOR);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/basic/Aggregator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/Aggregator.java b/core/src/main/java/brooklyn/enricher/basic/Aggregator.java
index 3e896db..cb80431 100644
--- a/core/src/main/java/brooklyn/enricher/basic/Aggregator.java
+++ b/core/src/main/java/brooklyn/enricher/basic/Aggregator.java
@@ -27,10 +27,8 @@ import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import brooklyn.catalog.Catalog;
 import brooklyn.config.BrooklynLogging;
 import brooklyn.config.ConfigKey;
-import brooklyn.config.BrooklynLogging.LoggingLevel;
 import brooklyn.entity.Entity;
 import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.event.AttributeSensor;
@@ -40,8 +38,11 @@ import brooklyn.event.SensorEventListener;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.text.StringPredicates;
 
 import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
 import com.google.common.reflect.TypeToken;
 
@@ -54,6 +55,7 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv
 
     public static final ConfigKey<Sensor<?>> SOURCE_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.sourceSensor");
     public static final ConfigKey<Function<? super Collection<?>, ?>> TRANSFORMATION = ConfigKeys.newConfigKey(new TypeToken<Function<? super Collection<?>, ?>>() {}, "enricher.transformation");
+    public static final ConfigKey<Boolean> EXCLUDE_BLANK = ConfigKeys.newBooleanConfigKey("enricher.aggregator.excludeBlank", "Whether explicit nulls or blank strings should be excluded (default false); this only applies if no value filter set", false);
 
     protected Sensor<T> sourceSensor;
     protected Function<? super Collection<T>, ? extends U> transformation;
@@ -71,7 +73,7 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv
     protected void setEntityLoadingConfig() {
         super.setEntityLoadingConfig();
         this.sourceSensor = (Sensor<T>) getRequiredConfig(SOURCE_SENSOR);
-        this.transformation = (Function<? super Collection<T>, ? extends U>) getRequiredConfig(TRANSFORMATION);
+        this.transformation = (Function<? super Collection<T>, ? extends U>) config().get(TRANSFORMATION);
     }
         
     @Override
@@ -124,6 +126,14 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv
     }
     
     @Override
+    protected Predicate<?> getDefaultValueFilter() {
+        if (getConfig(EXCLUDE_BLANK))
+            return StringPredicates.isNonBlank();
+        else
+            return Predicates.alwaysTrue();
+    }
+    
+    @Override
     protected void onProducerRemoved(Entity producer) {
         values.remove(producer);
         onUpdated();
@@ -156,6 +166,7 @@ public class Aggregator<T,U> extends AbstractAggregator<T,U> implements SensorEv
         synchronized (values) {
             // TODO Could avoid copying when filter not needed
             List<T> vs = MutableList.copyOf(Iterables.filter(values.values(), valueFilter));
+            if (transformation==null) return vs;
             return transformation.apply(vs);
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/basic/Joiner.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/Joiner.java b/core/src/main/java/brooklyn/enricher/basic/Joiner.java
new file mode 100644
index 0000000..5189273
--- /dev/null
+++ b/core/src/main/java/brooklyn/enricher/basic/Joiner.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 brooklyn.enricher.basic;
+
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.Sensor;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.event.basic.BasicSensorEvent;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.text.StringEscapes;
+import brooklyn.util.text.Strings;
+
+import com.google.common.reflect.TypeToken;
+
+//@Catalog(name="Transformer", description="Transforms attributes of an entity; see Enrichers.builder().transforming(...)")
+@SuppressWarnings("serial")
+public class Joiner<T> extends AbstractEnricher implements SensorEventListener<T> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Joiner.class);
+
+    public static ConfigKey<Entity> PRODUCER = ConfigKeys.newConfigKey(Entity.class, "enricher.producer");
+    public static ConfigKey<Sensor<?>> SOURCE_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.sourceSensor");
+    public static ConfigKey<Sensor<?>> TARGET_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.targetSensor");
+    @SetFromFlag("separator")
+    public static ConfigKey<String> SEPARATOR = ConfigKeys.newStringConfigKey("enricher.joiner.separator", "Separator string to insert between each argument", ",");
+    @SetFromFlag("quote")
+    public static ConfigKey<Boolean> QUOTE = ConfigKeys.newBooleanConfigKey("enricher.joiner.quote", "Whether to bash-escape each parameter and wrap in double-quotes, defaulting to true", true);
+    @SetFromFlag("minimum")
+    public static ConfigKey<Integer> MINIMUM = ConfigKeys.newIntegerConfigKey("enricher.joiner.minimum", "Minimum number of elements to join; if fewer than this, sets null; default 0 (no minimum)");
+    @SetFromFlag("maximum")
+    public static ConfigKey<Integer> MAXIMUM = ConfigKeys.newIntegerConfigKey("enricher.joiner.maximum", "Maximum number of elements to join; default null means all elements always taken");
+    
+//    protected Function<? super SensorEvent<T>, ? extends U> transformation;
+    protected Entity producer;
+    protected AttributeSensor<T> sourceSensor;
+    protected Sensor<String> targetSensor;
+
+    public Joiner() {
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Override
+    public void setEntity(EntityLocal entity) {
+        super.setEntity(entity);
+
+        this.producer = getConfig(PRODUCER) == null ? entity: getConfig(PRODUCER);
+        this.sourceSensor = (AttributeSensor<T>) getRequiredConfig(SOURCE_SENSOR);
+        this.targetSensor = (Sensor<String>) getRequiredConfig(TARGET_SENSOR);
+        
+        subscribe(producer, sourceSensor, this);
+        
+        Object value = producer.getAttribute((AttributeSensor<?>)sourceSensor);
+        // TODO would be useful to have a convenience to "subscribeAndThenIfItIsAlreadySetRunItOnce"
+        if (value!=null) {
+            onEvent(new BasicSensorEvent(sourceSensor, producer, value, -1));
+        }
+    }
+
+    @Override
+    public void onEvent(SensorEvent<T> event) {
+        emit(targetSensor, compute(event));
+    }
+
+    protected Object compute(SensorEvent<T> event) {
+        Object v = event.getValue();
+        Object result = null;
+        if (v!=null) {
+            if (v instanceof Map) {
+                v = ((Map<?,?>)v).values();
+            }
+            if (!(v instanceof Iterable)) {
+                LOG.warn("Enricher "+this+" received a non-iterable value "+v.getClass()+" "+v+"; refusing to join");
+            } else {
+                MutableList<Object> c1 = MutableList.of();
+                Integer maximum = getConfig(MAXIMUM);
+                for (Object ci: (Iterable<?>)v) {
+                    if (maximum!=null && maximum>=0) {
+                        if (c1.size()>=maximum) break;
+                    }
+                    c1.appendIfNotNull(Strings.toString(ci));
+                }
+                Integer minimum = getConfig(MINIMUM);
+                if (minimum!=null && c1.size() < minimum) {
+                    // use default null return value
+                } else {
+                    if (getConfig(QUOTE)) {
+                        MutableList<Object> c2 = MutableList.of();
+                        for (Object ci: c1) {
+                            c2.add(StringEscapes.BashStringEscapes.wrapBash((String)ci));
+                        }
+                        c1 = c2;
+                    }
+                    result = Strings.join(c1, getConfig(SEPARATOR));
+                }
+            }
+        }
+        if (LOG.isTraceEnabled())
+            LOG.trace("Enricher "+this+" computed "+result+" from "+event);
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/basic/Propagator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/Propagator.java b/core/src/main/java/brooklyn/enricher/basic/Propagator.java
index 5e3ae23..fd012a3 100644
--- a/core/src/main/java/brooklyn/enricher/basic/Propagator.java
+++ b/core/src/main/java/brooklyn/enricher/basic/Propagator.java
@@ -34,6 +34,7 @@ import brooklyn.event.AttributeSensor;
 import brooklyn.event.Sensor;
 import brooklyn.event.SensorEvent;
 import brooklyn.event.SensorEventListener;
+import brooklyn.util.collections.MutableMap;
 import brooklyn.util.flags.SetFromFlag;
 
 import com.google.common.base.Preconditions;
@@ -42,7 +43,6 @@ import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
 import com.google.common.reflect.TypeToken;
 
 @SuppressWarnings("serial")
@@ -73,6 +73,7 @@ public class Propagator extends AbstractEnricher implements SensorEventListener<
     protected Entity producer;
     protected Map<? extends Sensor<?>, ? extends Sensor<?>> sensorMapping;
     protected boolean propagatingAll;
+    protected Collection<Sensor<?>> propagatingAllBut;
     protected Predicate<Sensor<?>> sensorFilter;
 
     public Propagator() {
@@ -83,44 +84,62 @@ public class Propagator extends AbstractEnricher implements SensorEventListener<
         super.setEntity(entity);
         
         this.producer = getConfig(PRODUCER) == null ? entity : getConfig(PRODUCER);
+        boolean sensorMappingSet = getConfig(SENSOR_MAPPING)!=null;
+        MutableMap<Sensor<?>,Sensor<?>> sensorMappingTemp = MutableMap.copyOf(getConfig(SENSOR_MAPPING)); 
+        this.propagatingAll = Boolean.TRUE.equals(getConfig(PROPAGATING_ALL)) || getConfig(PROPAGATING_ALL_BUT)!=null;
+        
         if (getConfig(PROPAGATING) != null) {
-            if (Boolean.TRUE.equals(getConfig(PROPAGATING_ALL)) || getConfig(PROPAGATING_ALL_BUT) != null) {
+            if (propagatingAll) {
                 throw new IllegalStateException("Propagator enricher "+this+" must not have 'propagating' set at same time as either 'propagatingAll' or 'propagatingAllBut'");
             }
             
-            Map<Sensor<?>, Sensor<?>> sensorMappingTemp = Maps.newLinkedHashMap();
-            if (getConfig(SENSOR_MAPPING) != null) {
-                sensorMappingTemp.putAll(getConfig(SENSOR_MAPPING));
-            }
             for (Sensor<?> sensor : getConfig(PROPAGATING)) {
                 if (!sensorMappingTemp.containsKey(sensor)) {
                     sensorMappingTemp.put(sensor, sensor);
                 }
             }
             this.sensorMapping = ImmutableMap.copyOf(sensorMappingTemp);
-            this.propagatingAll = false;
             this.sensorFilter = new Predicate<Sensor<?>>() {
                 @Override public boolean apply(Sensor<?> input) {
-                    return input != null && sensorMapping.keySet().contains(input);
+                    // TODO kept for deserialization of inner classes, but shouldn't be necessary, as with other inner classes (qv);
+                    // NB: previously this did this check:
+//                    return input != null && sensorMapping.keySet().contains(input);
+                    // but those clauses seems wrong (when would input be null?) and unnecessary (we are doing an explicit subscribe in this code path) 
+                    return true;
                 }
             };
-        } else if (getConfig(PROPAGATING_ALL_BUT) == null) {
-            this.sensorMapping = getConfig(SENSOR_MAPPING) == null ? ImmutableMap.<Sensor<?>, Sensor<?>>of() : getConfig(SENSOR_MAPPING);
-            this.propagatingAll = Boolean.TRUE.equals(getConfig(PROPAGATING_ALL));
+        } else if (sensorMappingSet) {
+            if (propagatingAll) {
+                throw new IllegalStateException("Propagator enricher "+this+" must not have 'sensorMapping' set at same time as either 'propagatingAll' or 'propagatingAllBut'");
+            }
+            this.sensorMapping = ImmutableMap.copyOf(sensorMappingTemp);
             this.sensorFilter = Predicates.alwaysTrue();
         } else {
-            this.sensorMapping = getConfig(SENSOR_MAPPING) == null ? ImmutableMap.<Sensor<?>, Sensor<?>>of() : getConfig(SENSOR_MAPPING);
-            this.propagatingAll = true;
+            this.sensorMapping = ImmutableMap.<Sensor<?>, Sensor<?>>of();
+            if (!propagatingAll) {
+                // default if nothing specified is to do all but the ones not usually propagated
+                propagatingAll = true;
+                // user specified nothing, so *set* the all_but to the default set
+                // if desired, we could allow this to be dynamically reconfigurable, remove this field and always look up;
+                // slight performance hit (always looking up), and might need to recompute subscriptions, so not supported currently
+                propagatingAllBut = SENSORS_NOT_USUALLY_PROPAGATED;
+            } else {
+                propagatingAllBut = getConfig(PROPAGATING_ALL_BUT);
+            }
             this.sensorFilter = new Predicate<Sensor<?>>() {
                 @Override public boolean apply(Sensor<?> input) {
-                    Collection<Sensor<?>> exclusions = getConfig(PROPAGATING_ALL_BUT);
-                    return input != null && !exclusions.contains(input);
+                    Collection<Sensor<?>> exclusions = propagatingAllBut;
+                    // TODO this anonymous inner class and getConfig check kept should be removed / confirmed for rebind compatibility.
+                    // we *should* be regenerating these fields on each rebind (calling to this method), 
+                    // so serialization of this class shouldn't be needed (and should be skipped), but that needs to be checked.
+                    if (propagatingAllBut==null) exclusions = getConfig(PROPAGATING_ALL_BUT);
+                    return input != null && (exclusions==null || !exclusions.contains(input));
                 }
             };
         }
             
         Preconditions.checkState(propagatingAll ^ sensorMapping.size() > 0,
-                "Exactly one must be set of propagatingAll (%s, excluding %s), sensorMapping (%s)", propagatingAll, getConfig(PROPAGATING_ALL_BUT), sensorMapping);
+                "Nothing to propagate; detected: propagatingAll (%s, excluding %s), sensorMapping (%s)", propagatingAll, getConfig(PROPAGATING_ALL_BUT), sensorMapping);
 
         if (propagatingAll) {
             subscribe(producer, null, this);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/main/java/brooklyn/enricher/basic/Transformer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/Transformer.java b/core/src/main/java/brooklyn/enricher/basic/Transformer.java
index 86911e7..0811fcc 100644
--- a/core/src/main/java/brooklyn/enricher/basic/Transformer.java
+++ b/core/src/main/java/brooklyn/enricher/basic/Transformer.java
@@ -26,24 +26,29 @@ import org.slf4j.LoggerFactory;
 import brooklyn.config.ConfigKey;
 import brooklyn.entity.Entity;
 import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.Sensor;
 import brooklyn.event.SensorEvent;
 import brooklyn.event.SensorEventListener;
 import brooklyn.event.basic.BasicSensorEvent;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.Duration;
 
 import com.google.common.base.Function;
 import com.google.common.reflect.TypeToken;
 
-//@Catalog(name="Transformer", description="Transformers attributes of an entity; see Enrichers.builder().transforming(...)")
+//@Catalog(name="Transformer", description="Transforms attributes of an entity; see Enrichers.builder().transforming(...)")
 @SuppressWarnings("serial")
 public class Transformer<T,U> extends AbstractEnricher implements SensorEventListener<T> {
 
     private static final Logger LOG = LoggerFactory.getLogger(Transformer.class);
 
+    // exactly one of these should be supplied to set a value
+    public static ConfigKey<?> TARGET_VALUE = ConfigKeys.newConfigKey(Object.class, "enricher.targetValue");
     public static ConfigKey<Function<?, ?>> TRANSFORMATION_FROM_VALUE = ConfigKeys.newConfigKey(new TypeToken<Function<?, ?>>() {}, "enricher.transformation");
-    
     public static ConfigKey<Function<?, ?>> TRANSFORMATION_FROM_EVENT = ConfigKeys.newConfigKey(new TypeToken<Function<?, ?>>() {}, "enricher.transformation.fromevent");
     
     public static ConfigKey<Entity> PRODUCER = ConfigKeys.newConfigKey(Entity.class, "enricher.producer");
@@ -52,7 +57,7 @@ public class Transformer<T,U> extends AbstractEnricher implements SensorEventLis
 
     public static ConfigKey<Sensor<?>> TARGET_SENSOR = ConfigKeys.newConfigKey(new TypeToken<Sensor<?>>() {}, "enricher.targetSensor");
     
-    protected Function<? super SensorEvent<T>, ? extends U> transformation;
+//    protected Function<? super SensorEvent<T>, ? extends U> transformation;
     protected Entity producer;
     protected Sensor<T> sourceSensor;
     protected Sensor<U> targetSensor;
@@ -64,20 +69,8 @@ public class Transformer<T,U> extends AbstractEnricher implements SensorEventLis
     @Override
     public void setEntity(EntityLocal entity) {
         super.setEntity(entity);
-        
-        final Function<? super T, ? extends U> transformationFromValue = (Function<? super T, ? extends U>) getConfig(TRANSFORMATION_FROM_VALUE);
-        final Function<? super SensorEvent<T>, ? extends U> transformationFromEvent = (Function<? super SensorEvent<T>, ? extends U>) getConfig(TRANSFORMATION_FROM_EVENT);
-        checkArgument(transformationFromEvent != null ^ transformationFromValue != null, "must set exactly one of %s or %s", TRANSFORMATION_FROM_VALUE.getName(), TRANSFORMATION_FROM_EVENT.getName());
-        if (transformationFromEvent != null) {
-            transformation = transformationFromEvent;
-        } else {
-            // TODO new named class
-            transformation = new Function<SensorEvent<T>, U>() {
-                @Override public U apply(SensorEvent<T> input) {
-                    return transformationFromValue.apply(input.getValue());
-                }
-            };
-        }
+
+        Function<SensorEvent<T>, U> transformation = getTransformation();
         this.producer = getConfig(PRODUCER) == null ? entity: getConfig(PRODUCER);
         this.sourceSensor = (Sensor<T>) getRequiredConfig(SOURCE_SENSOR);
         Sensor<?> targetSensorSpecified = getConfig(TARGET_SENSOR);
@@ -102,13 +95,65 @@ public class Transformer<T,U> extends AbstractEnricher implements SensorEventLis
         }
     }
 
+    /** returns a function for transformation, for immediate use only (not for caching, as it may change) */
+    @SuppressWarnings("unchecked")
+    protected Function<SensorEvent<T>, U> getTransformation() {
+        MutableSet<Object> suppliers = MutableSet.of();
+        suppliers.addIfNotNull(config().getRaw(TARGET_VALUE).orNull());
+        suppliers.addIfNotNull(config().getRaw(TRANSFORMATION_FROM_EVENT).orNull());
+        suppliers.addIfNotNull(config().getRaw(TRANSFORMATION_FROM_VALUE).orNull());
+        checkArgument(suppliers.size()==1,  
+            "Must set exactly one of: %s, %s, %s", TARGET_VALUE.getName(), TRANSFORMATION_FROM_VALUE.getName(), TRANSFORMATION_FROM_EVENT.getName());
+        
+        Function<?, ?> fromEvent = config().get(TRANSFORMATION_FROM_EVENT);
+        if (fromEvent != null) {  
+            return (Function<SensorEvent<T>, U>) fromEvent;
+        }
+        
+        final Function<T, U> fromValueFn = (Function<T, U>) config().get(TRANSFORMATION_FROM_VALUE);
+        if (fromValueFn != null) {
+            // named class not necessary as result should not be serialized
+            return new Function<SensorEvent<T>, U>() {
+                @Override public U apply(SensorEvent<T> input) {
+                    return fromValueFn.apply(input.getValue());
+                }
+                @Override
+                public String toString() {
+                    return ""+fromValueFn;
+                }
+            };
+        }
+
+        // from target value
+        // named class not necessary as result should not be serialized
+        final Object targetValueRaw = config().getRaw(TARGET_VALUE).orNull();
+        return new Function<SensorEvent<T>, U>() {
+            @Override public U apply(SensorEvent<T> input) {
+                // evaluate immediately, or return null
+                // 200ms seems a reasonable compromise for tasks which require BG evaluation
+                // but which are non-blocking
+                // TODO better would be to have a mode in which tasks are not permitted to block on
+                // external events; they can submit tasks and block on them (or even better, have a callback architecture);
+                // however that is a non-trivial refactoring
+                return (U) Tasks.resolving(targetValueRaw).as(targetSensor.getType())
+                    .context( ((EntityInternal)entity).getExecutionContext() )
+                    .description("Computing sensor "+targetSensor+" from "+targetValueRaw)
+                    .timeout(Duration.millis(200))
+                    .getMaybe().orNull();
+            }
+            public String toString() {
+                return ""+targetValueRaw;
+            }
+        };
+    }
+
     @Override
     public void onEvent(SensorEvent<T> event) {
         emit(targetSensor, compute(event));
     }
 
     protected Object compute(SensorEvent<T> event) {
-        U result = transformation.apply(event);
+        U result = getTransformation().apply(event);
         if (LOG.isTraceEnabled())
             LOG.trace("Enricher "+this+" computed "+result+" from "+event);
         return result;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/core/src/test/java/brooklyn/enricher/EnrichersTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/enricher/EnrichersTest.java b/core/src/test/java/brooklyn/enricher/EnrichersTest.java
index 12f1cad..43d55c3 100644
--- a/core/src/test/java/brooklyn/enricher/EnrichersTest.java
+++ b/core/src/test/java/brooklyn/enricher/EnrichersTest.java
@@ -40,6 +40,7 @@ import brooklyn.test.Asserts;
 import brooklyn.test.EntityTestUtils;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.CollectionFunctionals;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.collections.MutableSet;
 import brooklyn.util.guava.Functionals;
@@ -250,6 +251,9 @@ public class EnrichersTest extends BrooklynAppUnitTestSupport {
         
         entity2.setAttribute(STR1, "myval");
         EntityTestUtils.assertAttributeEqualsEventually(entity, STR1, "myval");
+        
+        entity2.setAttribute(STR1, null);
+        EntityTestUtils.assertAttributeEqualsEventually(entity, STR1, null);
     }
     
     @Test
@@ -431,4 +435,45 @@ public class EnrichersTest extends BrooklynAppUnitTestSupport {
         EntityTestUtils.assertAttributeEqualsEventually(entity, mapSensor, MutableMap.<String,String>of());
     }
 
+    private static AttributeSensor<Object> LIST_SENSOR = Sensors.newSensor(Object.class, "sensor.list");
+    
+    @Test
+    public void testJoinerDefault() {
+        entity.addEnricher(Enrichers.builder()
+                .joining(LIST_SENSOR)
+                .publishing(TestEntity.NAME)
+                .build());
+        // null values ignored, and it quotes
+        entity.setAttribute(LIST_SENSOR, MutableList.<String>of("a", "\"b").append(null));
+        EntityTestUtils.assertAttributeEqualsEventually(entity, TestEntity.NAME, "\"a\",\"\\\"b\"");
+        
+        // empty list causes ""
+        entity.setAttribute(LIST_SENSOR, MutableList.<String>of().append(null));
+        EntityTestUtils.assertAttributeEqualsEventually(entity, TestEntity.NAME, "");
+        
+        // null causes null
+        entity.setAttribute(LIST_SENSOR, null);
+        EntityTestUtils.assertAttributeEqualsEventually(entity, TestEntity.NAME, null);
+    }
+
+    
+    @Test
+    public void testJoinerUnquoted() {
+        entity.setAttribute(LIST_SENSOR, MutableList.<String>of("a", "\"b", "ccc").append(null));
+        entity.addEnricher(Enrichers.builder()
+            .joining(LIST_SENSOR)
+            .publishing(TestEntity.NAME)
+            .minimum(1)
+            .maximum(2)
+            .separator(":")
+            .quote(false)
+            .build());
+        // in this case, it should be immediately available upon adding the enricher
+        EntityTestUtils.assertAttributeEquals(entity, TestEntity.NAME, "a:\"b");
+        
+        // empty list causes null here, because below the minimum
+        entity.setAttribute(LIST_SENSOR, MutableList.<String>of().append(null));
+        EntityTestUtils.assertAttributeEqualsEventually(entity, TestEntity.NAME, null);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/docs/guide/yaml/example_yaml/test-app-with-enrichers-slightly-simpler.yaml
----------------------------------------------------------------------
diff --git a/docs/guide/yaml/example_yaml/test-app-with-enrichers-slightly-simpler.yaml b/docs/guide/yaml/example_yaml/test-app-with-enrichers-slightly-simpler.yaml
new file mode 100644
index 0000000..a6a8116
--- /dev/null
+++ b/docs/guide/yaml/example_yaml/test-app-with-enrichers-slightly-simpler.yaml
@@ -0,0 +1,57 @@
+#
+# example showing how enrichers can be set 
+# 
+name: test-app-with-enrichers
+description: Testing many enrichers
+services:
+- type: brooklyn.entity.group.DynamicCluster
+  id: cluster
+  initialSize: 3
+  location: localhost
+  memberSpec:
+    $brooklyn:entitySpec:
+      type: brooklyn.test.entity.TestEntity
+      brooklyn.enrichers:
+      - type: brooklyn.enricher.basic.Transformer
+        # transform "ip" (which we expect a feed, not shown here, to set) to a URL;
+        # you can curl an address string to the sensors/ip endpoint an entity to trigger these enrichers 
+        brooklyn.config:
+          enricher.sourceSensor: $brooklyn:sensor("ip")
+          enricher.targetSensor: $brooklyn:sensor("url")
+          enricher.targetValue: $brooklyn:formatString("http://%s/", $brooklyn:attributeWhenReady("ip"))
+      - type: brooklyn.enricher.basic.Propagator
+        # use propagator to duplicate one sensor as another, giving the supplied sensor mapping;
+        # the other use of Propagator is where you specify a producer (using $brooklyn:entity(...) as below)
+        # from which to take sensors; in that mode you can specify `propagate` as a list of sensors whose names are unchanged,
+        # instead of (or in addition to) this map 
+        brooklyn.config:
+          sensorMapping:
+            $brooklyn:sensor("url"): $brooklyn:sensor("brooklyn.entity.basic.Attributes", "main.uri")
+  brooklyn.enrichers:
+  - type: brooklyn.enricher.basic.Aggregator
+    # aggregate `url` sensors from children into a list
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("url")
+      enricher.targetSensor: $brooklyn:sensor("urls.list")
+      enricher.aggregating.fromMembers: true
+  - type: brooklyn.enricher.basic.Joiner
+    # create a string from that list, for use e.g. in bash scripts
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("urls.list")
+      enricher.targetSensor: $brooklyn:sensor("urls.list.comma_separated.max_2")
+      maximum: 2
+      # TODO infer uniqueTag, name etc
+      uniqueTag: urls.list.comma_separated.max_2
+  - type: brooklyn.enricher.basic.Joiner
+    # pick one uri as the main one to use
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("urls.list")
+      enricher.targetSensor: $brooklyn:sensor("brooklyn.entity.basic.Attributes", "main.uri")
+      quote: false
+      maximum: 1
+brooklyn.enrichers:
+- type: brooklyn.enricher.basic.Propagator
+  # if nothing specified for `propagating` or `sensorMapping` then 
+  # Propagator will do all but the usual lifecycle defaults, handy at the root!
+  brooklyn.config:
+    producer: $brooklyn:entity("cluster")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/docs/guide/yaml/yaml-reference.md
----------------------------------------------------------------------
diff --git a/docs/guide/yaml/yaml-reference.md b/docs/guide/yaml/yaml-reference.md
index 07fffc9..71993ae 100644
--- a/docs/guide/yaml/yaml-reference.md
+++ b/docs/guide/yaml/yaml-reference.md
@@ -37,7 +37,10 @@ the entity being defined, with these being the most common:
 
 * `brooklyn.policies`: a list of policies, each as a map described with their `type` and their `brooklyn.config` as keys
 
-* `brooklyn.enrichers`: a list of enrichers, each as a map described with their `type` and their `brooklyn.config` as keys
+* `brooklyn.enrichers`: a list of enrichers, each as a map described with their `type` and their `brooklyn.config` as keys;
+  see the keys declared on individual enrichers; 
+  also see [this enricher example](example_yaml/test-app-with-enrichers-slightly-simpler.yaml) for a detailed and commented illustration
+  <!-- TODO assert that this yaml maches the yaml we test against -->
 
 * `brooklyn.initializers`: a list of `EntityInitializer` instances to be constructed and run against the entity, 
   each as a map described with their `type` and their `brooklyn.config` as keys.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java b/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
index 9cf3233..d661403 100644
--- a/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
+++ b/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/BrooklynDslInterpreter.java
@@ -115,8 +115,10 @@ public class BrooklynDslInterpreter extends PlanInterpreterAdapter {
                 try {
                     // TODO in future we should support functions of the form 'Maps.clear', 'Maps.reset', 'Maps.remove', etc;
                     // default approach only supported if mapIn has single item and mapOut is empty
-                    if (mapIn.size()!=1) throw new IllegalStateException("Map-entry DSL syntax only supported with single item in map, not "+mapIn);
-                    if (mapOut.size()!=0) throw new IllegalStateException("Map-entry DSL syntax only supported with empty output map-so-far, not "+mapOut);
+                    if (mapIn.size()!=1) 
+                        throw new IllegalStateException("Map-entry DSL syntax only supported with single item in map, not "+mapIn);
+                    if (mapOut.size()!=0) 
+                        throw new IllegalStateException("Map-entry DSL syntax only supported with empty output map-so-far, not "+mapOut);
 
                     node.setNewValue( evaluate(new FunctionWithArgs(f.getFunction(), args), false) );
                     return false;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java b/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
index da19f6e..108cd98 100644
--- a/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
+++ b/usage/camp/src/main/java/io/brooklyn/camp/brooklyn/spi/dsl/methods/BrooklynDslCommon.java
@@ -101,7 +101,7 @@ public class BrooklynDslCommon {
         return new DslComponent(Scope.THIS, "").sensor(sensorName);
     }
     
-    /** Returns a {@link Sensor} from the given entity type. */
+    /** Returns a {@link Sensor} declared on the type (e.g. entity class) declared in the first argument. */
     @SuppressWarnings({ "unchecked", "rawtypes" })
     public static Sensor<?> sensor(String clazzName, String sensorName) {
         try {
@@ -117,6 +117,7 @@ public class BrooklynDslCommon {
                 sensor = sensors.get(sensorName);
             }
             if (sensor == null) {
+                // TODO could extend API to return a sensor of the given type; useful but makes API ambiguous in theory (unlikely in practise, but still...)
                 throw new IllegalArgumentException("Sensor " + sensorName + " not found on class " + clazzName);
             }
             return sensor;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java
new file mode 100644
index 0000000..99165c5
--- /dev/null
+++ b/usage/camp/src/test/java/io/brooklyn/camp/brooklyn/EnrichersSlightlySimplerYamlTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 io.brooklyn.camp.brooklyn;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.event.basic.Sensors;
+import brooklyn.test.EntityTestUtils;
+import brooklyn.util.collections.CollectionFunctionals;
+import brooklyn.util.text.StringPredicates;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+
+/** Tests some improvements to enricher classes to make them a bit more yaml friendly.
+ * Called "SlightlySimpler" as it would be nice to make enrichers a lot more yaml friendly! */
+@Test
+public class EnrichersSlightlySimplerYamlTest extends AbstractYamlTest {
+    private static final Logger log = LoggerFactory.getLogger(EnrichersSlightlySimplerYamlTest.class);
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Test
+    public void testWithAppEnricher() throws Exception {
+        Entity app = createAndStartApplication(loadYaml("test-app-with-enrichers-slightly-simpler.yaml"));
+        waitForApplicationTasks(app);
+        log.info("Started "+app+":");
+        Entities.dumpInfo(app);
+        
+        Entity cluster = Iterables.getOnlyElement( app.getChildren() );
+        Collection<Entity> leafs = ((DynamicCluster)cluster).getMembers();
+        Iterator<Entity> li = leafs.iterator();
+        
+        Entity e1 = li.next();
+        ((EntityInternal)e1).setAttribute(Sensors.newStringSensor("ip"), "127.0.0.1");
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Sensors.newStringSensor("url"), "http://127.0.0.1/");
+        EntityTestUtils.assertAttributeEqualsEventually(e1, Attributes.MAIN_URI, URI.create("http://127.0.0.1/"));
+
+        int i=2;
+        while (li.hasNext()) {
+            Entity ei = li.next();
+            ((EntityInternal)ei).setAttribute(Sensors.newStringSensor("ip"), "127.0.0."+i);
+            i++;
+        }
+        
+        EntityTestUtils.assertAttributeEventually(cluster, Sensors.newSensor(Iterable.class, "urls.list"),
+            (Predicate)CollectionFunctionals.sizeEquals(3));
+        
+        EntityTestUtils.assertAttributeEventually(cluster, Sensors.newSensor(String.class, "urls.list.comma_separated.max_2"),
+            StringPredicates.matchesRegex("\"http:\\/\\/127[^\"]*\\/\",\"http:\\/\\/127[^\"]*\\/\""));
+
+        EntityTestUtils.assertAttributeEventually(cluster, Attributes.MAIN_URI, Predicates.notNull());
+        URI main = cluster.getAttribute(Attributes.MAIN_URI);
+        Assert.assertTrue(main.toString().matches("http:\\/\\/127.0.0..\\/"), "Wrong URI: "+main);
+        
+        EntityTestUtils.assertAttributeEventually(app, Attributes.MAIN_URI, Predicates.notNull());
+        main = app.getAttribute(Attributes.MAIN_URI);
+        Assert.assertTrue(main.toString().matches("http:\\/\\/127.0.0..\\/"), "Wrong URI: "+main);
+        
+        // TODO would we want to allow "all-but-usual" as the default if nothing specified
+    }
+    
+    @Override
+    protected Logger getLogger() {
+        return log;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/usage/camp/src/test/resources/test-app-with-enrichers-slightly-simpler.yaml
----------------------------------------------------------------------
diff --git a/usage/camp/src/test/resources/test-app-with-enrichers-slightly-simpler.yaml b/usage/camp/src/test/resources/test-app-with-enrichers-slightly-simpler.yaml
new file mode 100644
index 0000000..df725e3
--- /dev/null
+++ b/usage/camp/src/test/resources/test-app-with-enrichers-slightly-simpler.yaml
@@ -0,0 +1,74 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+# example showing how enrichers can be set 
+#
+name: test-app-with-enrichers
+description: Testing many enrichers
+services:
+- type: brooklyn.entity.group.DynamicCluster
+  id: cluster
+  initialSize: 3
+  location: localhost
+  memberSpec:
+    $brooklyn:entitySpec:
+      type: brooklyn.test.entity.TestEntity
+      brooklyn.enrichers:
+      - type: brooklyn.enricher.basic.Transformer
+        # transform "ip" (which we expect a feed, not shown here, to set) to a URL;
+        # you can curl an address string to the sensors/ip endpoint an entity to trigger these enrichers 
+        brooklyn.config:
+          enricher.sourceSensor: $brooklyn:sensor("ip")
+          enricher.targetSensor: $brooklyn:sensor("url")
+          enricher.targetValue: $brooklyn:formatString("http://%s/", $brooklyn:attributeWhenReady("ip"))
+      - type: brooklyn.enricher.basic.Propagator
+        # use propagator to duplicate one sensor as another, giving the supplied sensor mapping;
+        # the other use of Propagator is where you specify a producer (using $brooklyn:entity(...) as below)
+        # from which to take sensors; in that mode you can specify `propagate` as a list of sensors whose names are unchanged,
+        # instead of (or in addition to) this map 
+        brooklyn.config:
+          sensorMapping:
+            $brooklyn:sensor("url"): $brooklyn:sensor("brooklyn.entity.basic.Attributes", "main.uri")
+  brooklyn.enrichers:
+  - type: brooklyn.enricher.basic.Aggregator
+    # aggregate `url` sensors from children into a list
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("url")
+      enricher.targetSensor: $brooklyn:sensor("urls.list")
+      enricher.aggregating.fromMembers: true
+  - type: brooklyn.enricher.basic.Joiner
+    # create a string from that list, for use e.g. in bash scripts
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("urls.list")
+      enricher.targetSensor: $brooklyn:sensor("urls.list.comma_separated.max_2")
+      maximum: 2
+      # TODO infer uniqueTag, name etc
+      uniqueTag: urls.list.comma_separated.max_2
+  - type: brooklyn.enricher.basic.Joiner
+    # pick one uri as the main one to use
+    brooklyn.config:
+      enricher.sourceSensor: $brooklyn:sensor("urls.list")
+      enricher.targetSensor: $brooklyn:sensor("brooklyn.entity.basic.Attributes", "main.uri")
+      quote: false
+      maximum: 1
+brooklyn.enrichers:
+- type: brooklyn.enricher.basic.Propagator
+  # if nothing specified for `propagating` or `sensorMapping` then 
+  # Propagator will do all but the usual lifecycle defaults, handy at the root!
+  brooklyn.config:
+    producer: $brooklyn:entity("cluster")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f7142a33/utils/common/src/main/java/brooklyn/util/text/StringPredicates.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/text/StringPredicates.java b/utils/common/src/main/java/brooklyn/util/text/StringPredicates.java
index 15a306a..f83b139 100644
--- a/utils/common/src/main/java/brooklyn/util/text/StringPredicates.java
+++ b/utils/common/src/main/java/brooklyn/util/text/StringPredicates.java
@@ -67,6 +67,28 @@ public class StringPredicates {
         };
     }
 
+
+    /** Tests if object is non-null and not a blank string.
+     * <p>
+     * Predicate form of {@link Strings#isNonBlank(CharSequence)} also accepting objects non-null, for convenience */
+    public static <T> Predicate<T> isNonBlank() {
+        return new IsNonBlank<T>();
+    }
+
+    private static final class IsNonBlank<T> implements Predicate<T> {
+        @Override
+        public boolean apply(@Nullable Object input) {
+            if (input==null) return false;
+            if (!(input instanceof CharSequence)) return true;
+            return Strings.isNonBlank((CharSequence)input);
+        }
+
+        @Override
+        public String toString() {
+            return "isNonBlank()";
+        }
+    }
+    
     // -----------------
     
     public static <T extends CharSequence> Predicate<T> containsLiteralIgnoreCase(final String fragment) {