http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
 
b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
new file mode 100644
index 0000000..c9a05ef
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityConfigMap.java
@@ -0,0 +1,306 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.internal;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.brooklyn.util.GroovyJavaMethods.elvis;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.api.mgmt.ExecutionContext;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.config.ConfigInheritance;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.Sanitizer;
+import org.apache.brooklyn.core.config.StructuredConfigKey;
+import org.apache.brooklyn.core.config.internal.AbstractConfigMapImpl;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.flags.FlagUtils;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.internal.ConfigKeySelfExtracting;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+public class EntityConfigMap extends AbstractConfigMapImpl {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(EntityConfigMap.class);
+
+    /** entity against which config resolution / task execution will occur */
+    private final AbstractEntity entity;
+
+    /**
+     * Map of configuration information that is defined at start-up time for 
the entity. These
+     * configuration parameters are shared and made accessible to the 
"children" of this
+     * entity.
+     */
+    private final Map<ConfigKey<?>,Object> inheritedConfig = 
Collections.synchronizedMap(new LinkedHashMap<ConfigKey<?>, Object>());
+    // TODO do we really want to have *both* bags and maps for these?  danger 
that they get out of synch.
+    // have added some logic (Oct 2014) so that the same changes are applied 
to both, in most places at least;
+    // i (alex) think we should prefer ConfigBag (the input keys don't matter, 
it is more a question of retrieval keys),
+    // but first we need ConfigBag to support StructuredConfigKeys 
+    private final ConfigBag localConfigBag;
+    private final ConfigBag inheritedConfigBag;
+
+    public EntityConfigMap(AbstractEntity entity, Map<ConfigKey<?>, Object> 
storage) {
+        this.entity = checkNotNull(entity, "entity must be specified");
+        this.ownConfig = checkNotNull(storage, "storage map must be 
specified");
+        
+        // TODO store ownUnused in backing-storage
+        this.localConfigBag = ConfigBag.newInstance();
+        this.inheritedConfigBag = ConfigBag.newInstance();
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T getConfig(ConfigKey<T> key, T defaultValue) {
+        // FIXME What about inherited task in config?!
+        //              alex says: think that should work, no?
+        // FIXME What if someone calls getConfig on a task, before setting 
parent app?
+        //              alex says: not supported (throw exception, or return 
the task)
+        
+        // In case this entity class has overridden the given key (e.g. to set 
default), then retrieve this entity's key
+        // TODO If ask for a config value that's not in our configKeys, should 
we really continue with rest of method and return key.getDefaultValue?
+        //      e.g. SshBasedJavaAppSetup calls setAttribute(JMX_USER), which 
calls getConfig(JMX_USER)
+        //           but that example doesn't have a default...
+        ConfigKey<T> ownKey = entity!=null ? 
(ConfigKey<T>)elvis(entity.getEntityType().getConfigKey(key.getName()), key) : 
key;
+        
+        ConfigInheritance inheritance = key.getInheritance();
+        if (inheritance==null) inheritance = ownKey.getInheritance(); 
+        if (inheritance==null) {
+            // TODO we could warn by introducing a temporary 
"ALWAYS_BUT_WARNING" instance
+            inheritance = getDefaultInheritance(); 
+        }
+        
+        // TODO We're notifying of config-changed because currently 
persistence needs to know when the
+        // attributeWhenReady is complete (so it can persist the result).
+        // Long term, we'll just persist tasks properly so the call to 
onConfigChanged will go!
+
+        // Don't use groovy truth: if the set value is e.g. 0, then would 
ignore set value and return default!
+        if (ownKey instanceof ConfigKeySelfExtracting) {
+            Object rawval = ownConfig.get(key);
+            T result = null;
+            boolean complete = false;
+            if (((ConfigKeySelfExtracting<T>)ownKey).isSet(ownConfig)) {
+                ExecutionContext exec = entity.getExecutionContext();
+                result = 
((ConfigKeySelfExtracting<T>)ownKey).extractValue(ownConfig, exec);
+                complete = true;
+            } else if (isInherited(ownKey, inheritance) && 
+                    
((ConfigKeySelfExtracting<T>)ownKey).isSet(inheritedConfig)) {
+                ExecutionContext exec = entity.getExecutionContext();
+                result = 
((ConfigKeySelfExtracting<T>)ownKey).extractValue(inheritedConfig, exec);
+                complete = true;
+            } else if (localConfigBag.containsKey(ownKey)) {
+                // TODO configBag.get doesn't handle tasks/attributeWhenReady 
- it only uses TypeCoercions
+                result = localConfigBag.get(ownKey);
+                complete = true;
+            } else if (isInherited(ownKey, inheritance) && 
+                    inheritedConfigBag.containsKey(ownKey)) {
+                result = inheritedConfigBag.get(ownKey);
+                complete = true;
+            }
+
+            if (rawval instanceof Task) {
+                
entity.getManagementSupport().getEntityChangeListener().onConfigChanged(key);
+            }
+            if (complete) {
+                return result;
+            }
+        } else {
+            LOG.warn("Config key {} of {} is not a ConfigKeySelfExtracting; 
cannot retrieve value; returning default", ownKey, this);
+        }
+        return TypeCoercions.coerce((defaultValue != null) ? defaultValue : 
ownKey.getDefaultValue(), key.getTypeToken());
+    }
+
+    private <T> boolean isInherited(ConfigKey<T> key) {
+        return isInherited(key, key.getInheritance());
+    }
+    private <T> boolean isInherited(ConfigKey<T> key, ConfigInheritance 
inheritance) {
+        if (inheritance==null) inheritance = getDefaultInheritance(); 
+        return inheritance.isInherited(key, entity.getParent(), entity);
+    }
+    private ConfigInheritance getDefaultInheritance() {
+        return ConfigInheritance.ALWAYS; 
+    }
+
+    @Override
+    public Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean 
includeInherited) {
+        if (ownConfig.containsKey(key)) return Maybe.of(ownConfig.get(key));
+        if (includeInherited && inheritedConfig.containsKey(key)) return 
Maybe.of(inheritedConfig.get(key));
+        return Maybe.absent();
+    }
+    
+    /** an immutable copy of the config visible at this entity, local and 
inherited (preferring local) */
+    public Map<ConfigKey<?>,Object> getAllConfig() {
+        Map<ConfigKey<?>,Object> result = new 
LinkedHashMap<ConfigKey<?>,Object>(inheritedConfig.size()+ownConfig.size());
+        result.putAll(inheritedConfig);
+        result.putAll(ownConfig);
+        return Collections.unmodifiableMap(result);
+    }
+
+    /** an immutable copy of the config defined at this entity, ie not 
inherited */
+    public Map<ConfigKey<?>,Object> getLocalConfig() {
+        Map<ConfigKey<?>,Object> result = new 
LinkedHashMap<ConfigKey<?>,Object>(ownConfig.size());
+        result.putAll(ownConfig);
+        return Collections.unmodifiableMap(result);
+    }
+    
+    /** Creates an immutable copy of the config visible at this entity, local 
and inherited (preferring local), including those that did not match config 
keys */
+    public ConfigBag getAllConfigBag() {
+        return ConfigBag.newInstanceCopying(localConfigBag)
+                .putAll(ownConfig)
+                .putIfAbsent(inheritedConfig)
+                .putIfAbsent(inheritedConfigBag)
+                .seal();
+    }
+
+    /** Creates an immutable copy of the config defined at this entity, ie not 
inherited, including those that did not match config keys */
+    public ConfigBag getLocalConfigBag() {
+        return ConfigBag.newInstanceCopying(localConfigBag)
+                .putAll(ownConfig)
+                .seal();
+    }
+
+    @SuppressWarnings("unchecked")
+    public Object setConfig(ConfigKey<?> key, Object v) {
+        Object val = coerceConfigVal(key, v);
+        Object oldVal;
+        if (key instanceof StructuredConfigKey) {
+            oldVal = ((StructuredConfigKey)key).applyValueToMap(val, 
ownConfig);
+            // TODO ConfigBag does not handle structured config keys; quick 
fix is to remove (and should also remove any subkeys;
+            // as it stands if someone set string a.b.c in the config bag then 
removed structured key a.b, then got a.b.c they'd get a vale);
+            // long term fix is to support structured config keys in 
ConfigBag, at which point i think we could remove ownConfig altogether
+            localConfigBag.remove(key);
+        } else {
+            oldVal = ownConfig.put(key, val);
+            localConfigBag.put((ConfigKey<Object>)key, v);
+        }
+        entity.config().refreshInheritedConfigOfChildren();
+        return oldVal;
+    }
+    
+    public void setLocalConfig(Map<ConfigKey<?>, ?> vals) {
+        ownConfig.clear();
+        localConfigBag.clear();
+        ownConfig.putAll(vals);
+        localConfigBag.putAll(vals);
+    }
+    
+    public void setInheritedConfig(Map<ConfigKey<?>, ?> valsO, ConfigBag 
configBagVals) {
+        Map<ConfigKey<?>, ?> vals = filterUninheritable(valsO);
+        
+        inheritedConfig.clear();
+        inheritedConfig.putAll(vals);
+
+        // The configBagVals contains all inherited, including strings that 
did not match a config key on the parent.
+        // They might match a config-key on this entity though, so need to 
check that:
+        //   - if it matches one of our keys, set it in inheritedConfig
+        //   - otherwise add it to our inheritedConfigBag
+        Set<String> valKeyNames = Sets.newLinkedHashSet();
+        for (ConfigKey<?> key : vals.keySet()) {
+            valKeyNames.add(key.getName());
+        }
+        Map<String,Object> valsUnmatched = MutableMap.<String,Object>builder()
+                .putAll(configBagVals.getAllConfig())
+                .removeAll(valKeyNames)
+                .build();
+        inheritedConfigBag.clear();
+        Map<ConfigKey<?>, SetFromFlag> annotatedConfigKeys = 
FlagUtils.getAnnotatedConfigKeys(entity.getClass());
+        Map<String, ConfigKey<?>> renamedConfigKeys = Maps.newLinkedHashMap();
+        for (Map.Entry<ConfigKey<?>, SetFromFlag> entry: 
annotatedConfigKeys.entrySet()) {
+            String rename = entry.getValue().value();
+            if (rename != null) {
+                renamedConfigKeys.put(rename, entry.getKey());
+            }
+        }
+        for (Map.Entry<String,Object> entry : valsUnmatched.entrySet()) {
+            String name = entry.getKey();
+            Object value = entry.getValue();
+            ConfigKey<?> key = renamedConfigKeys.get(name);
+            if (key == null) key = entity.getEntityType().getConfigKey(name);
+            if (key != null) {
+                if (!isInherited(key)) {
+                    // no-op
+                } else if (inheritedConfig.containsKey(key)) {
+                    LOG.warn("Entity "+entity+" inherited duplicate config for 
key "+key+", via explicit config and string name "+name+"; using value of key");
+                } else {
+                    inheritedConfig.put(key, value);
+                }
+            } else {
+                // a config bag has discarded the keys, so we must assume 
default inheritance for things given that way
+                // unless we can infer a key; not a big deal, as we should 
have the key in inheritedConfig for everything
+                // which originated with a key ... but still, it would be nice 
to clean up the use of config bag!
+                inheritedConfigBag.putStringKey(name, value);
+            }
+        }
+    }
+    
+    private Map<ConfigKey<?>, ?> filterUninheritable(Map<ConfigKey<?>, ?> 
vals) {
+        Map<ConfigKey<?>, Object> result = Maps.newLinkedHashMap();
+        for (Map.Entry<ConfigKey<?>, ?> entry : vals.entrySet()) {
+            if (isInherited(entry.getKey())) {
+                result.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return result;
+    }
+    
+    public void addToLocalBag(Map<String,?> vals) {
+        localConfigBag.putAll(vals);
+        // quick fix for problem that ownConfig can get out of synch
+        ownConfig.putAll(localConfigBag.getAllConfigAsConfigKeyMap());
+    }
+
+    public void removeFromLocalBag(String key) {
+        localConfigBag.remove(key);
+        ownConfig.remove(key);
+    }
+
+    public void clearInheritedConfig() {
+        inheritedConfig.clear();
+        inheritedConfigBag.clear();
+    }
+
+    @Override
+    public EntityConfigMap submap(Predicate<ConfigKey<?>> filter) {
+        EntityConfigMap m = new EntityConfigMap(entity, Maps.<ConfigKey<?>, 
Object>newLinkedHashMap());
+        for (Map.Entry<ConfigKey<?>,Object> entry: inheritedConfig.entrySet())
+            if (filter.apply(entry.getKey()))
+                m.inheritedConfig.put(entry.getKey(), entry.getValue());
+        for (Map.Entry<ConfigKey<?>,Object> entry: ownConfig.entrySet())
+            if (filter.apply(entry.getKey()))
+                m.ownConfig.put(entry.getKey(), entry.getValue());
+        return m;
+    }
+
+    @Override
+    public String toString() {
+        return super.toString()+"[own="+Sanitizer.sanitize(ownConfig)+"; 
inherited="+Sanitizer.sanitize(inheritedConfig)+"]";
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityTransientCopyInternal.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityTransientCopyInternal.java
 
b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityTransientCopyInternal.java
new file mode 100644
index 0000000..09a8fdf
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/internal/EntityTransientCopyInternal.java
@@ -0,0 +1,121 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.internal;
+
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityType;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ExecutionContext;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
+import org.apache.brooklyn.api.mgmt.rebind.mementos.EntityMemento;
+import org.apache.brooklyn.api.objs.BrooklynObject.TagSupport;
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.Enricher;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.entity.EntityInternal.FeedSupport;
+import org.apache.brooklyn.core.mgmt.internal.EntityManagementSupport;
+import org.apache.brooklyn.core.objs.proxy.EntityProxyImpl;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.guava.Maybe;
+
+import com.google.common.annotations.Beta;
+
+/** 
+ * Selected methods from {@link EntityInternal} and parents which are permitted
+ * for entities being loaded in read-only mode, enforced by {@link 
EntityProxyImpl}.
+ * <p>
+ * Some of these methods do expose write capabilities, but such modifications 
are likely
+ * to be temporary, discarded on next rebind. Callers must take care with any 
such invocations.
+ * (The primary intent of this interface is to catch and prevent *most* such 
invocations!)
+ */
+@Beta
+public interface EntityTransientCopyInternal {
+
+    // TODO For feeds() and config(), need to ensure mutator methods on 
returned object are not invoked.
+    
+    // from Entity
+    
+    String getId();
+    long getCreationTime();
+    String getDisplayName();
+    @Nullable String getIconUrl();
+    EntityType getEntityType();
+    Application getApplication();
+    String getApplicationId();
+    Entity getParent();
+    Collection<Entity> getChildren();
+    Collection<Policy> getPolicies();
+    Collection<Enricher> getEnrichers();
+    Collection<Group> getGroups();
+    Collection<Location> getLocations();
+    <T> T getAttribute(AttributeSensor<T> sensor);
+    <T> T getConfig(ConfigKey<T> key);
+    <T> T getConfig(HasConfigKey<T> key);
+    Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited);
+    Maybe<Object> getConfigRaw(HasConfigKey<?> key, boolean includeInherited);
+    TagSupport tags();
+    String getCatalogItemId();
+
+    
+    // from entity local
+    
+    @Deprecated <T> T getConfig(ConfigKey<T> key, T defaultValue);
+    @Deprecated <T> T getConfig(HasConfigKey<T> key, T defaultValue);
+
+    
+    // from EntityInternal:
+    
+    @Deprecated EntityConfigMap getConfigMap();
+    @Deprecated Map<ConfigKey<?>,Object> getAllConfig();
+    // for rebind mainly:
+    @Deprecated ConfigBag getAllConfigBag();
+    @Deprecated ConfigBag getLocalConfigBag();
+    @SuppressWarnings("rawtypes")
+    Map<AttributeSensor, Object> getAllAttributes();
+    EntityManagementSupport getManagementSupport();
+    ManagementContext getManagementContext();
+    Effector<?> getEffector(String effectorName);
+    @Deprecated FeedSupport getFeedSupport();
+    FeedSupport feeds();
+    RebindSupport<EntityMemento> getRebindSupport();
+    // for REST calls on read-only entities which want to resolve values
+    ExecutionContext getExecutionContext();
+    void setCatalogItemId(String id);
+    
+    /** more methods, but which are only on selected entities */
+    public interface SpecialEntityTransientCopyInternal {
+        // from Group
+        Collection<Entity> getMembers();
+        boolean hasMember(Entity member);
+        Integer getCurrentSize();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/Lifecycle.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/Lifecycle.java 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/Lifecycle.java
new file mode 100644
index 0000000..dfbdbd7
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/Lifecycle.java
@@ -0,0 +1,185 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.lifecycle;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.core.config.render.RendererHints;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.text.StringFunctions;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+
+/**
+ * An enumeration representing the status of an {@link 
org.apache.brooklyn.api.entity.Entity}.
+ */
+public enum Lifecycle {
+    /**
+     * The entity has just been created.
+     *
+     * This stage encompasses the contruction. Once this stage is
+     * complete, the basic set of {@link brooklyn.event.Sensor}s will be 
available, apart from any that require the entity to be active or
+     * deployed to a {@link Location}.
+     */
+    CREATED,
+
+    /**
+     * The entity is starting.
+     * <p>
+     * This stage is typically entered when the {@link 
brooklyn.entity.trait.Startable#START} {@link brooklyn.entity.Effector} 
+     * is called, to undertake the startup operations from the management 
plane.
+     * When this completes the entity will normally transition to 
+     * {@link Lifecycle#RUNNING}. 
+     */
+    STARTING,
+
+    /**
+     * The entity service is expected to be running. In healthy operation, 
{@link Attributes#SERVICE_UP} will be true,
+     * or will shortly be true if all service start actions have been 
completed and we are merely waiting for it to be running. 
+     */
+    RUNNING,
+
+    /**
+     * The entity is stopping.
+     *
+     * This stage is activated when the {@link 
brooklyn.entity.trait.Startable#STOP} effector is called. The entity service is 
stopped. 
+     * Sensors that provide data from the running entity may be cleared and 
subscriptions cancelled.
+     */
+    STOPPING,
+
+    /**
+     * The entity is not expected to be active.
+     *
+     * This stage is entered when an entity is stopped, or may be entered when 
an entity is 
+     * fully created but not started. It may or may not be removed from the 
location(s) it was assigned,
+     * and it will typically not be providing new sensor data apart.
+     */
+    STOPPED,
+
+    /**
+     * The entity is destroyed.
+     *
+     * The entity will be unmanaged and removed from any groups and from its 
parent.
+     */
+    DESTROYED,
+
+    /**
+     * Entity error state.
+     *
+     * This stage is reachable from any other stage if an error occurs or an 
exception is thrown.
+     */
+    ON_FIRE;
+
+    /**
+     * The text representation of the {@link #name()}.
+     *
+     * This is formatted as lower case characters, with hyphens instead of 
spaces.
+     */
+    public String value() {
+       return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, name());
+    }
+
+    /** @see #value() */
+    @Override
+    public String toString() { return value(); }
+
+    /**
+     * Creates a {@link Lifecycle} from a text representation.
+     *
+     * This accepts the text representations output by the {@link #value()} 
method for each entry.
+     *
+     * @see #value()
+     */
+    public static Lifecycle fromValue(String v) {
+       try {
+          return 
valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, v));
+       } catch (IllegalArgumentException iae) {
+          return ON_FIRE;
+       }
+    }
+    
+    public static class Transition implements Serializable {
+        private static final long serialVersionUID = 603419184398753502L;
+        
+        final Lifecycle state;
+        final long timestampUtc;
+        
+        public Transition(Lifecycle state, Date timestamp) {
+            this.state = Preconditions.checkNotNull(state, "state");
+            this.timestampUtc = Preconditions.checkNotNull(timestamp, 
"timestamp").getTime();
+        }
+        
+        public Lifecycle getState() {
+            return state;
+        }
+        public Date getTimestamp() {
+            return new Date(timestampUtc);
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(state, timestampUtc);
+        }
+        
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof Transition)) return false;
+            if (!state.equals(((Transition)obj).getState())) return false;
+            if (timestampUtc != ((Transition)obj).timestampUtc) return false;
+            return true;
+        }
+        
+        @Override
+        public String toString() {
+            return state+" @ "+timestampUtc+" / "+new Date(timestampUtc);
+        }
+    }
+    
+    protected static class TransitionCoalesceFunction implements 
Function<String, Transition> {
+        private static final Pattern TRANSITION_PATTERN = 
Pattern.compile("^([\\w-]+)\\s+@\\s+(\\d+).*");
+
+        @Override
+        public Transition apply(final String input) {
+            if (input != null) {
+                Matcher m = TRANSITION_PATTERN.matcher(input);
+                if (m.matches()) {
+                    Lifecycle state = 
Lifecycle.valueOf(m.group(1).toUpperCase().replace('-', '_'));
+                    long time = Long.parseLong(m.group(2));
+                    return new Transition(state, new Date(time));
+                } else {
+                    throw new IllegalStateException("Serialized 
Lifecycle.Transition can't be parsed: " + input);
+                }
+            } else {
+                return null;
+            }
+        }
+    }
+
+    static {
+        TypeCoercions.registerAdapter(String.class, Transition.class, new 
TransitionCoalesceFunction());
+        RendererHints.register(Transition.class, 
RendererHints.displayValue(StringFunctions.toStringFunction()));
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/PolicyDescriptor.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/PolicyDescriptor.java
 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/PolicyDescriptor.java
new file mode 100644
index 0000000..ee063cb
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/PolicyDescriptor.java
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.lifecycle;
+
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.core.entity.AbstractEntity;
+
+import com.google.common.base.Objects;
+
+/** Emitted as part of {@link AbstractEntity#POLICY_ADDED} and {@link 
AbstractEntity#POLICY_REMOVED} */
+public class PolicyDescriptor {
+
+    private final String id;
+    private final String type;
+    private final String name;
+
+    public PolicyDescriptor(Policy policy) {
+        this.id = policy.getId();
+        this.type = policy.getPolicyType().getName();
+        this.name = policy.getDisplayName();
+    }
+    public String getId() {
+        return id;
+    }
+    
+    public String getPolicyType() {
+        return type;
+    }
+    
+    public String getName() {
+        return name;
+    }
+    
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof PolicyDescriptor)) {
+            return false;
+        }
+        PolicyDescriptor o = (PolicyDescriptor) other;
+        return Objects.equal(id, o.id) && Objects.equal(type, o.type) && 
Objects.equal(name, o.name);
+    }
+    
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+    
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this).add("id", id).add("type", 
type).add("name",  name).omitNullValues().toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/QuorumCheck.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/QuorumCheck.java 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/QuorumCheck.java
new file mode 100644
index 0000000..29f8deb
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/QuorumCheck.java
@@ -0,0 +1,108 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.lifecycle;
+
+import java.io.Serializable;
+
+/**
+ * For checking if a group/cluster is quorate. That is, whether the group has 
sufficient
+ * healthy members.
+ * @deprecated since 0.7.0 use {@link 
org.apache.brooklyn.util.collections.QuorumCheck}. 
+ * but keep this for a while as old quorum checks might be persisted. 
+ */
+@Deprecated
+public interface QuorumCheck extends 
org.apache.brooklyn.util.collections.QuorumCheck {
+
+    /**
+     * @param sizeHealthy Number of healthy members
+     * @param totalSize   Total number of members one would expect to be 
healthy (i.e. ignoring stopped members)
+     * @return            Whether this group is healthy
+     */
+    public boolean isQuorate(int sizeHealthy, int totalSize);
+
+    public static class QuorumChecks {
+        /**
+         * Checks that all members that should be up are up (i.e. ignores 
stopped nodes).
+         */
+        public static QuorumCheck all() {
+            return new NumericQuorumCheck(0, 1.0, false);
+        }
+        /**
+         * Checks all members that should be up are up, and that there is at 
least one such member.
+         */
+        public static QuorumCheck allAndAtLeastOne() {
+            return new NumericQuorumCheck(1, 1.0, false);
+        }
+        /**
+         * Requires at least one member that should be up is up.
+         */
+        public static QuorumCheck atLeastOne() {
+            return new NumericQuorumCheck(1, 0.0, false);
+        }
+        /**
+         * Requires at least one member to be up if the total size is non-zero.
+         * i.e. okay if empty, or if non-empty and something is healthy, but 
not okay if not-empty and nothing is healthy.
+         * "Empty" means that no members are supposed to be up  (e.g. there 
may be stopped members).
+         */
+        public static QuorumCheck atLeastOneUnlessEmpty() {
+            return new NumericQuorumCheck(1, 0.0, true);
+        }
+        /**
+         * Always "healthy"
+         */
+        public static QuorumCheck alwaysTrue() {
+            return new NumericQuorumCheck(0, 0.0, true);
+        }
+        public static QuorumCheck newInstance(int minRequiredSize, double 
minRequiredRatio, boolean allowEmpty) {
+            return new NumericQuorumCheck(minRequiredSize, minRequiredRatio, 
allowEmpty);
+        }
+    }
+    
+    /** @deprecated since 0.7.0 use {@link 
org.apache.brooklyn.util.collections.QuorumCheck}. 
+    * but keep this until we have a transition defined. 
+    */
+    @Deprecated
+    public static class NumericQuorumCheck implements QuorumCheck, 
Serializable {
+        private static final long serialVersionUID = -5090669237460159621L;
+        
+        protected final int minRequiredSize;
+        protected final double minRequiredRatio;
+        protected final boolean allowEmpty;
+
+        public NumericQuorumCheck(int minRequiredSize, double 
minRequiredRatio, boolean allowEmpty) {
+            this.minRequiredSize = minRequiredSize;
+            this.minRequiredRatio = minRequiredRatio;
+            this.allowEmpty = allowEmpty;
+        }
+        
+        @Override
+        public boolean isQuorate(int sizeHealthy, int totalSize) {
+            if (allowEmpty && totalSize==0) return true;
+            if (sizeHealthy < minRequiredSize) return false;
+            if (sizeHealthy < totalSize*minRequiredRatio-0.000000001) return 
false;
+            return true;
+        }
+        
+        @Override
+        public String toString() {
+            return 
"QuorumCheck[require="+minRequiredSize+","+((int)100*minRequiredRatio)+"%"+(allowEmpty
 ? "|0" : "")+"]";
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..654662f
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/lifecycle/ServiceStateLogic.java
@@ -0,0 +1,639 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.lifecycle;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.Enricher;
+import org.apache.brooklyn.api.sensor.EnricherSpec;
+import org.apache.brooklyn.api.sensor.EnricherSpec.ExtensibleEnricherSpec;
+import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.api.sensor.SensorEvent;
+import org.apache.brooklyn.api.sensor.SensorEventListener;
+import org.apache.brooklyn.config.ConfigInheritance;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.BrooklynLogging;
+import org.apache.brooklyn.core.BrooklynLogging.LoggingLevel;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityAdjuncts;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.entity.EntityPredicates;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle.Transition;
+import org.apache.brooklyn.sensor.enricher.AbstractEnricher;
+import org.apache.brooklyn.sensor.enricher.AbstractMultipleSensorAggregator;
+import org.apache.brooklyn.sensor.enricher.Enrichers;
+import org.apache.brooklyn.sensor.enricher.UpdatingMap;
+import org.apache.brooklyn.util.collections.CollectionFunctionals;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.collections.QuorumCheck;
+import org.apache.brooklyn.util.core.task.ValueResolver;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+
+/** Logic, sensors and enrichers, and conveniences, for computing service 
status */ 
+public class ServiceStateLogic {
+
+    private static final Logger log = 
LoggerFactory.getLogger(ServiceStateLogic.class);
+    
+    public static final AttributeSensor<Boolean> SERVICE_UP = 
Attributes.SERVICE_UP;
+    public static final AttributeSensor<Map<String,Object>> 
SERVICE_NOT_UP_INDICATORS = Attributes.SERVICE_NOT_UP_INDICATORS;
+    public static final AttributeSensor<Map<String,Object>> 
SERVICE_NOT_UP_DIAGNOSTICS = Attributes.SERVICE_NOT_UP_DIAGNOSTICS;
+    
+    public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = 
Attributes.SERVICE_STATE_ACTUAL;
+    public static final AttributeSensor<Lifecycle.Transition> 
SERVICE_STATE_EXPECTED = Attributes.SERVICE_STATE_EXPECTED;
+    public static final AttributeSensor<Map<String,Object>> SERVICE_PROBLEMS = 
Attributes.SERVICE_PROBLEMS;
+
+    /** static only; not for instantiation */
+    private ServiceStateLogic() {}
+
+    public static <TKey,TVal> TVal getMapSensorEntry(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor, TKey key) {
+        Map<TKey, TVal> map = entity.getAttribute(sensor);
+        if (map==null) return null;
+        return map.get(key);
+    }
+    
+    @SuppressWarnings("unchecked")
+    public static <TKey,TVal> void clearMapSensorEntry(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor, TKey key) {
+        updateMapSensorEntry(entity, sensor, key, (TVal)Entities.REMOVE);
+    }
+
+    /** update the given key in the given map sensor */
+    public static <TKey,TVal> void updateMapSensorEntry(EntityLocal entity, 
AttributeSensor<Map<TKey,TVal>> sensor, final TKey key, final TVal v) {
+        /*
+         * Important to *not* modify the existing attribute value; must make a 
copy, modify that, and publish.
+         * This is because a Propagator enricher will set this same value on 
another entity. There was very
+         * strange behaviour when this was done for a SERVICE_UP_INDICATORS 
sensor - the updates done here 
+         * applied to the attribute of both entities!
+         * 
+         * Need to do this update atomically (i.e. sequentially) because there 
is no threading control for
+         * what is calling updateMapSensorEntity. It is called directly on 
start, on initialising enrichers,
+         * and in event listeners. These calls could be concurrent.
+         */
+        Function<Map<TKey,TVal>, Maybe<Map<TKey,TVal>>> modifier = new 
Function<Map<TKey,TVal>, Maybe<Map<TKey,TVal>>>() {
+            @Override public Maybe<Map<TKey, TVal>> apply(Map<TKey, TVal> map) 
{
+                boolean created = (map==null);
+                if (created) map = MutableMap.of();
+                
+                boolean changed;
+                if (v == Entities.REMOVE) {
+                    changed = map.containsKey(key);
+                    if (changed) {
+                        map = MutableMap.copyOf(map);
+                        map.remove(key);
+                    }
+                } else {
+                    TVal oldV = map.get(key);
+                    if (oldV==null) {
+                        changed = (v!=null || !map.containsKey(key));
+                    } else {
+                        changed = !oldV.equals(v);
+                    }
+                    if (changed) {
+                        map = MutableMap.copyOf(map);
+                        map.put(key, (TVal)v);
+                    }
+                }
+                if (changed || created) {
+                    return Maybe.of(map);
+                } else {
+                    return Maybe.absent();
+                }
+            }
+        };
+        
+        if (!Entities.isNoLongerManaged(entity)) { 
+            entity.modifyAttribute(sensor, modifier);
+        }
+    }
+    
+    public static void setExpectedState(Entity entity, Lifecycle state) {
+        if (state==Lifecycle.RUNNING) {
+            Boolean up = 
((EntityInternal)entity).getAttribute(Attributes.SERVICE_UP);
+            if (!Boolean.TRUE.equals(up) && 
!Boolean.TRUE.equals(Entities.isReadOnly(entity))) {
+                // pause briefly to allow any recent problem-clearing 
processing to complete
+                Stopwatch timer = Stopwatch.createStarted();
+                boolean nowUp = Repeater.create()
+                        .every(ValueResolver.REAL_QUICK_PERIOD)
+                        .limitTimeTo(ValueResolver.PRETTY_QUICK_WAIT)
+                        .until(entity, 
EntityPredicates.attributeEqualTo(Attributes.SERVICE_UP, true))
+                        .run();
+                if (nowUp) {
+                    log.debug("Had to wait "+Duration.of(timer)+" for 
"+entity+" "+Attributes.SERVICE_UP+" to be true before setting "+state);
+                } else {
+                    log.warn("Service is not up when setting "+state+" on 
"+entity+"; delayed "+Duration.of(timer)+" "
+                        + "but "+Attributes.SERVICE_UP+" did not recover from 
"+up+"; 
not-up-indicators="+entity.getAttribute(Attributes.SERVICE_NOT_UP_INDICATORS));
+                }
+            }
+        }
+        
((EntityInternal)entity).setAttribute(Attributes.SERVICE_STATE_EXPECTED, new 
Lifecycle.Transition(state, new Date()));
+        
+        Maybe<Enricher> enricher = 
EntityAdjuncts.tryFindWithUniqueTag(entity.getEnrichers(), 
ComputeServiceState.DEFAULT_ENRICHER_UNIQUE_TAG);
+        if (enricher.isPresent() && enricher.get() instanceof 
ComputeServiceState) {
+            ((ComputeServiceState)enricher.get()).onEvent(null);
+        }
+    }
+    public static Lifecycle getExpectedState(Entity entity) {
+        Transition expected = 
entity.getAttribute(Attributes.SERVICE_STATE_EXPECTED);
+        if (expected==null) return null;
+        return expected.getState();
+    }
+    public static boolean isExpectedState(Entity entity, Lifecycle state) {
+        return getExpectedState(entity)==state;
+    }
+    
+    public static class ServiceNotUpLogic {
+        public static final String DEFAULT_ENRICHER_UNIQUE_TAG = "service.isUp 
if no service.notUp.indicators";
+        
+        /** static only; not for instantiation */
+        private ServiceNotUpLogic() {}
+        
+        public static final EnricherSpec<?> 
newEnricherForServiceUpIfNotUpIndicatorsEmpty() {
+            return Enrichers.builder()
+                
.transforming(SERVICE_NOT_UP_INDICATORS).<Object>publishing(Attributes.SERVICE_UP)
+                .suppressDuplicates(true)
+                .computing(
+                    Functionals.<Map<String,?>>
+                        
ifNotEquals(null).<Object>apply(Functions.forPredicate(CollectionFunctionals.<String>mapSizeEquals(0)))
+                        .defaultValue(Entities.REMOVE) )
+                .uniqueTag(DEFAULT_ENRICHER_UNIQUE_TAG)
+                .build();
+        }
+        
+        /** puts the given value into the {@link 
Attributes#SERVICE_NOT_UP_INDICATORS} map as if the 
+         * {@link UpdatingMap} enricher for the given key */
+        public static void updateNotUpIndicator(EntityLocal entity, String 
key, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
key, value);
+        }
+        /** clears any entry for the given key in the {@link 
Attributes#SERVICE_NOT_UP_INDICATORS} map */
+        public static void clearNotUpIndicator(EntityLocal entity, String key) 
{
+            clearMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
key);
+        }
+        /** as {@link #updateNotUpIndicator(EntityLocal, String, Object)} 
using the given sensor as the key */
+        public static void updateNotUpIndicator(EntityLocal entity, Sensor<?> 
sensor, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
sensor.getName(), value);
+        }
+        /** as {@link #clearNotUpIndicator(EntityLocal, String)} using the 
given sensor as the key */
+        public static void clearNotUpIndicator(EntityLocal entity, Sensor<?> 
sensor) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_NOT_UP_INDICATORS, 
sensor.getName());
+        }
+
+        public static void 
updateNotUpIndicatorRequiringNonEmptyList(EntityLocal entity, AttributeSensor<? 
extends Collection<?>> collectionSensor) {
+            Collection<?> nodes = entity.getAttribute(collectionSensor);
+            if (nodes==null || nodes.isEmpty()) 
ServiceNotUpLogic.updateNotUpIndicator(entity, collectionSensor, "Should have 
at least one entry");
+            else ServiceNotUpLogic.clearNotUpIndicator(entity, 
collectionSensor);
+        }
+        public static void 
updateNotUpIndicatorRequiringNonEmptyMap(EntityLocal entity, AttributeSensor<? 
extends Map<?,?>> mapSensor) {
+            Map<?, ?> nodes = entity.getAttribute(mapSensor);
+            if (nodes==null || nodes.isEmpty()) 
ServiceNotUpLogic.updateNotUpIndicator(entity, mapSensor, "Should have at least 
one entry");
+            else ServiceNotUpLogic.clearNotUpIndicator(entity, mapSensor);
+        }
+        
+    }
+    
+    /** Enricher which sets {@link Attributes#SERVICE_STATE_ACTUAL} on changes 
to 
+     * {@link Attributes#SERVICE_STATE_EXPECTED}, {@link 
Attributes#SERVICE_PROBLEMS}, and {@link Attributes#SERVICE_UP}
+     * <p>
+     * The default implementation uses {@link 
#computeActualStateWhenExpectedRunning(Map, Boolean)} if the last expected 
transition
+     * was to {@link Lifecycle#RUNNING} and 
+     * {@link #computeActualStateWhenNotExpectedRunning(Map, Boolean, 
org.apache.brooklyn.core.entity.lifecycle.Lifecycle.Transition)} otherwise.
+     * If these methods return null, the {@link 
Attributes#SERVICE_STATE_ACTUAL} sensor will be cleared (removed).
+     * Either of these methods can be overridden for custom logic, and that 
custom enricher can be created using 
+     * {@link ServiceStateLogic#newEnricherForServiceState(Class)} and added 
to an entity.
+     */
+    public static class ComputeServiceState extends AbstractEnricher 
implements SensorEventListener<Object> {
+        
+        public static final String DEFAULT_ENRICHER_UNIQUE_TAG = 
"service.state.actual";
+
+        public ComputeServiceState() {}
+        public ComputeServiceState(Map<?,?> flags) { super(flags); }
+            
+        @Override
+        public void init() {
+            super.init();
+            if (uniqueTag==null) uniqueTag = DEFAULT_ENRICHER_UNIQUE_TAG;
+        }
+        
+        @Override
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            if (suppressDuplicates==null) {
+                // only publish on changes, unless it is configured otherwise
+                suppressDuplicates = true;
+            }
+            
+            subscribe(entity, SERVICE_PROBLEMS, this);
+            subscribe(entity, SERVICE_UP, this);
+            subscribe(entity, SERVICE_STATE_EXPECTED, this);
+            onEvent(null);
+        }
+
+        @Override
+        public void onEvent(@Nullable SensorEvent<Object> event) {
+            Preconditions.checkNotNull(entity, "Cannot handle subscriptions or 
compute state until associated with an entity");
+            
+            Map<String, Object> serviceProblems = 
entity.getAttribute(SERVICE_PROBLEMS);
+            Boolean serviceUp = entity.getAttribute(SERVICE_UP);
+            Lifecycle.Transition serviceExpected = 
entity.getAttribute(SERVICE_STATE_EXPECTED);
+            
+            if (serviceExpected!=null && 
serviceExpected.getState()==Lifecycle.RUNNING) {
+                setActualState( 
computeActualStateWhenExpectedRunning(serviceProblems, serviceUp) );
+            } else {
+                setActualState( 
computeActualStateWhenNotExpectedRunning(serviceProblems, serviceUp, 
serviceExpected) );
+            }
+        }
+
+        protected Lifecycle computeActualStateWhenExpectedRunning(Map<String, 
Object> problems, Boolean serviceUp) {
+            if (Boolean.TRUE.equals(serviceUp) && (problems==null || 
problems.isEmpty())) {
+                return Lifecycle.RUNNING;
+            } else {
+                if 
(!Lifecycle.ON_FIRE.equals(entity.getAttribute(SERVICE_STATE_ACTUAL))) {
+                    BrooklynLogging.log(log, 
BrooklynLogging.levelDependingIfReadOnly(entity, LoggingLevel.WARN, 
LoggingLevel.TRACE, LoggingLevel.DEBUG),
+                        "Setting "+entity+" "+Lifecycle.ON_FIRE+" due to 
problems when expected running, up="+serviceUp+", "+
+                            (problems==null || problems.isEmpty() ? 
"not-up-indicators: "+entity.getAttribute(SERVICE_NOT_UP_INDICATORS) : 
"problems: "+problems));
+                }
+                return Lifecycle.ON_FIRE;
+            }
+        }
+        
+        protected Lifecycle 
computeActualStateWhenNotExpectedRunning(Map<String, Object> problems, Boolean 
up, Lifecycle.Transition stateTransition) {
+            if (stateTransition!=null) {
+                // if expected state is present but not running, just echo the 
expected state (ignore problems and up-ness)
+                return stateTransition.getState();
+                
+            } else if (problems!=null && !problems.isEmpty()) {
+                // if there is no expected state, then if service is not up, 
say stopped, else say on fire (whether service up is true or not present)
+                if (Boolean.FALSE.equals(up)) {
+                    return Lifecycle.STOPPED;
+                } else {
+                    BrooklynLogging.log(log, 
BrooklynLogging.levelDependingIfReadOnly(entity, LoggingLevel.WARN, 
LoggingLevel.TRACE, LoggingLevel.DEBUG),
+                        "Setting "+entity+" "+Lifecycle.ON_FIRE+" due to 
problems when expected "+stateTransition+" / up="+up+": "+problems);
+                    return Lifecycle.ON_FIRE;
+                }
+            } else {
+                // no expected transition and no problems
+                // if the problems map is non-null, then infer from service up;
+                // if there is no problems map, then leave unchanged (user may 
have set it explicitly)
+                if (problems!=null)
+                    return (up==null ? null /* remove if up is not set */ : 
+                        up ? Lifecycle.RUNNING : Lifecycle.STOPPED);
+                else
+                    return entity.getAttribute(SERVICE_STATE_ACTUAL);
+            }
+        }
+
+        protected void setActualState(@Nullable Lifecycle state) {
+            if (log.isTraceEnabled()) log.trace("{} setting actual state {}", 
this, state);
+            if 
(((EntityInternal)entity).getManagementSupport().isNoLongerManaged()) {
+                // won't catch everything, but catches some
+                BrooklynLogging.log(log, 
BrooklynLogging.levelDebugOrTraceIfReadOnly(entity),
+                    entity+" is no longer managed when told to set actual 
state to "+state+"; suppressing");
+                return;
+            }
+            emit(SERVICE_STATE_ACTUAL, (state==null ? Entities.REMOVE : 
state));
+        }
+
+    }
+    
+    public static final EnricherSpec<?> 
newEnricherForServiceStateFromProblemsAndUp() {
+        return newEnricherForServiceState(ComputeServiceState.class);
+    }
+    public static final EnricherSpec<?> newEnricherForServiceState(Class<? 
extends Enricher> type) {
+        return EnricherSpec.create(type);
+    }
+    
+    public static class ServiceProblemsLogic {
+        /** static only; not for instantiation */
+        private ServiceProblemsLogic() {}
+        
+        /** puts the given value into the {@link Attributes#SERVICE_PROBLEMS} 
map as if the 
+         * {@link UpdatingMap} enricher for the given sensor reported this 
value */
+        public static void updateProblemsIndicator(EntityLocal entity, 
Sensor<?> sensor, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
sensor.getName(), value);
+        }
+        /** clears any entry for the given sensor in the {@link 
Attributes#SERVICE_PROBLEMS} map */
+        public static void clearProblemsIndicator(EntityLocal entity, 
Sensor<?> sensor) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
sensor.getName());
+        }
+        /** as {@link #updateProblemsIndicator(EntityLocal, Sensor, Object)} */
+        public static void updateProblemsIndicator(EntityLocal entity, 
Effector<?> eff, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
eff.getName(), value);
+        }
+        /** as {@link #clearProblemsIndicator(EntityLocal, Sensor)} */
+        public static void clearProblemsIndicator(EntityLocal entity, 
Effector<?> eff) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, 
eff.getName());
+        }
+        /** as {@link #updateProblemsIndicator(EntityLocal, Sensor, Object)} */
+        public static void updateProblemsIndicator(EntityLocal entity, String 
key, Object value) {
+            updateMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, key, 
value);
+        }
+        /** as {@link #clearProblemsIndicator(EntityLocal, Sensor)} */
+        public static void clearProblemsIndicator(EntityLocal entity, String 
key) {
+            clearMapSensorEntry(entity, Attributes.SERVICE_PROBLEMS, key);
+        }
+    }
+    
+    public static class ComputeServiceIndicatorsFromChildrenAndMembers extends 
AbstractMultipleSensorAggregator<Void> implements SensorEventListener<Object> {
+        /** standard unique tag identifying instances of this enricher at 
runtime, also used for the map sensor if no unique tag specified */
+        public final static String DEFAULT_UNIQUE_TAG = 
"service-lifecycle-indicators-from-children-and-members";
+        
+        /** as {@link #DEFAULT_UNIQUE_TAG}, but when a second distinct 
instance is responsible for computing service up */
+        public final static String DEFAULT_UNIQUE_TAG_UP = 
"service-not-up-indicators-from-children-and-members";
+
+        public static final ConfigKey<QuorumCheck> UP_QUORUM_CHECK = 
ConfigKeys.builder(QuorumCheck.class, 
"enricher.service_state.children_and_members.quorum.up")
+            .description("Logic for checking whether this service is up, based 
on children and/or members, defaulting to allowing none but if there are any 
requiring at least one to be up")
+            .defaultValue(QuorumCheck.QuorumChecks.atLeastOneUnlessEmpty())
+            .inheritance(ConfigInheritance.NONE)
+            .build();
+        public static final ConfigKey<QuorumCheck> RUNNING_QUORUM_CHECK = 
ConfigKeys.builder(QuorumCheck.class, 
"enricher.service_state.children_and_members.quorum.running") 
+            .description("Logic for checking whether this service is healthy, 
based on children and/or members running, defaulting to requiring none to be 
ON-FIRE")
+            .defaultValue(QuorumCheck.QuorumChecks.all())
+            .inheritance(ConfigInheritance.NONE)
+            .build();
+        // TODO items below should probably also have inheritance NONE ?
+        public static final ConfigKey<Boolean> DERIVE_SERVICE_NOT_UP = 
ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.service_up.publish",
 "Whether to derive a service-not-up indicator from children", true);
+        public static final ConfigKey<Boolean> DERIVE_SERVICE_PROBLEMS = 
ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.service_problems.publish",
 "Whether to derive a service-problem indicator from children", true);
+        public static final ConfigKey<Boolean> 
IGNORE_ENTITIES_WITH_SERVICE_UP_NULL = 
ConfigKeys.newBooleanConfigKey("enricher.service_state.children_and_members.ignore_entities.service_up_null",
 "Whether to ignore children reporting null values for service up", true);
+        @SuppressWarnings("serial")
+        public static final ConfigKey<Set<Lifecycle>> 
IGNORE_ENTITIES_WITH_THESE_SERVICE_STATES = ConfigKeys.newConfigKey(new 
TypeToken<Set<Lifecycle>>() {},
+            
"enricher.service_state.children_and_members.ignore_entities.service_state_values",
 
+            "Service states (including null) which indicate an entity should 
be ignored when looking at children service states; anything apart from RUNNING 
not in this list will be treated as not healthy (by default just ON_FIRE will 
mean not healthy)", 
+            
MutableSet.<Lifecycle>builder().addAll(Lifecycle.values()).add(null).remove(Lifecycle.RUNNING).remove(Lifecycle.ON_FIRE).build().asUnmodifiable());
+
+        protected String getKeyForMapSensor() {
+            return Preconditions.checkNotNull(super.getUniqueTag());
+        }
+
+        @Override
+        protected void setEntityLoadingConfig() {
+            fromChildren = true;
+            fromMembers = true;
+            // above sets default
+            super.setEntityLoadingConfig();
+            if (isAggregatingMembers() && (!(entity instanceof Group))) {
+                if (fromChildren) fromMembers=false;
+                else throw new IllegalStateException("Cannot monitor only 
members for non-group entity "+entity+": "+this);
+            }
+            Preconditions.checkNotNull(getKeyForMapSensor());
+        }
+
+        @Override
+        protected void setEntityLoadingTargetConfig() {
+            if (getConfig(TARGET_SENSOR)!=null)
+                throw new IllegalArgumentException("Must not set 
"+TARGET_SENSOR+" when using "+this);
+        }
+
+        @Override
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            if (suppressDuplicates==null) {
+                // only publish on changes, unless it is configured otherwise
+                suppressDuplicates = true;
+            }
+        }
+
+        final static Set<ConfigKey<?>> RECONFIGURABLE_KEYS = 
ImmutableSet.<ConfigKey<?>>of(
+            UP_QUORUM_CHECK, RUNNING_QUORUM_CHECK,
+            DERIVE_SERVICE_NOT_UP, DERIVE_SERVICE_NOT_UP, 
+            IGNORE_ENTITIES_WITH_SERVICE_UP_NULL, 
IGNORE_ENTITIES_WITH_THESE_SERVICE_STATES);
+        
+        @Override
+        protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
+            if (RECONFIGURABLE_KEYS.contains(key)) {
+                return;
+            } else {
+                super.doReconfigureConfig(key, val);
+            }
+        }
+        
+        @Override
+        protected void onChanged() {
+            super.onChanged();
+            if (entity != null && isRunning())
+                onUpdated();
+        }
+        
+        private final List<Sensor<?>> SOURCE_SENSORS = 
ImmutableList.<Sensor<?>>of(SERVICE_UP, SERVICE_STATE_ACTUAL);
+        @Override
+        protected Collection<Sensor<?>> getSourceSensors() {
+            return SOURCE_SENSORS;
+        }
+
+        @Override
+        protected void onUpdated() {
+            if (entity==null || !Entities.isManaged(entity)) {
+                // either invoked during setup or entity has become unmanaged; 
just ignore
+                BrooklynLogging.log(log, 
BrooklynLogging.levelDebugOrTraceIfReadOnly(entity),
+                    "Ignoring {} onUpdated when entity is not in valid state 
({})", this, entity);
+                return;
+            }
+
+            // override superclass to publish multiple sensors
+            if (getConfig(DERIVE_SERVICE_PROBLEMS)) {
+                updateMapSensor(SERVICE_PROBLEMS, computeServiceProblems());
+            }
+
+            if (getConfig(DERIVE_SERVICE_NOT_UP)) {
+                updateMapSensor(SERVICE_NOT_UP_INDICATORS, 
computeServiceNotUp());
+            }
+        }
+
+        protected Object computeServiceNotUp() {
+            Map<Entity, Boolean> values = getValues(SERVICE_UP);
+            List<Entity> violators = MutableList.of();
+            boolean ignoreNull = 
getConfig(IGNORE_ENTITIES_WITH_SERVICE_UP_NULL);
+            Set<Lifecycle> ignoreStates = 
getConfig(IGNORE_ENTITIES_WITH_THESE_SERVICE_STATES);
+            int entries=0;
+            int numUp=0;
+            for (Map.Entry<Entity, Boolean> state: values.entrySet()) {
+                if (ignoreNull && state.getValue()==null)
+                    continue;
+                entries++;
+                Lifecycle entityState = 
state.getKey().getAttribute(SERVICE_STATE_ACTUAL);
+                
+                if (Boolean.TRUE.equals(state.getValue())) numUp++;
+                else if (!ignoreStates.contains(entityState)) {
+                    violators.add(state.getKey());
+                }
+            }
+
+            QuorumCheck qc = getConfig(UP_QUORUM_CHECK);
+            if (qc!=null) {
+                if (qc.isQuorate(numUp, violators.size()+numUp))
+                    // quorate
+                    return null;
+
+                if (values.isEmpty()) return "No entities present";
+                if (entries==0) return "No entities publishing service up";
+                if (violators.isEmpty()) return "Not enough entities";
+            } else {
+                if (violators.isEmpty())
+                    return null;
+            }
+
+            if (violators.size()==1) return violators.get(0)+" is not up";
+            if (violators.size()==entries) return "None of the entities are 
up";
+            return violators.size()+" entities are not up, including 
"+violators.get(0);
+        }
+
+        protected Object computeServiceProblems() {
+            Map<Entity, Lifecycle> values = getValues(SERVICE_STATE_ACTUAL);
+            int numRunning=0;
+            List<Entity> onesNotHealthy=MutableList.of();
+            Set<Lifecycle> ignoreStates = 
getConfig(IGNORE_ENTITIES_WITH_THESE_SERVICE_STATES);
+            for (Map.Entry<Entity,Lifecycle> state: values.entrySet()) {
+                if (state.getValue()==Lifecycle.RUNNING) numRunning++;
+                else if (!ignoreStates.contains(state.getValue())) 
+                    onesNotHealthy.add(state.getKey());
+            }
+
+            QuorumCheck qc = getConfig(RUNNING_QUORUM_CHECK);
+            if (qc!=null) {
+                if (qc.isQuorate(numRunning, onesNotHealthy.size()+numRunning))
+                    // quorate
+                    return null;
+
+                if (onesNotHealthy.isEmpty())
+                    return "Not enough entities running to be quorate";
+            } else {
+                if (onesNotHealthy.isEmpty())
+                    return null;
+            }
+
+            return "Required entit"+Strings.ies(onesNotHealthy.size())+" not 
healthy: "+
+                (onesNotHealthy.size()>3 ? onesNotHealthy.get(0)+" and 
"+(onesNotHealthy.size()-1)+" others"
+                    : Strings.join(onesNotHealthy, ", "));
+        }
+
+        protected void updateMapSensor(AttributeSensor<Map<String, Object>> 
sensor, Object value) {
+            if (log.isTraceEnabled()) log.trace("{} updating map sensor {} 
with {}", new Object[] { this, sensor, value });
+
+            if (value!=null) {
+                updateMapSensorEntry(entity, sensor, getKeyForMapSensor(), 
value);
+            } else {
+                clearMapSensorEntry(entity, sensor, getKeyForMapSensor());
+            }
+        }
+
+        /** not used; see specific `computeXxx` methods, invoked by overridden 
onUpdated */
+        @Override
+        protected Object compute() {
+            return null;
+        }
+    }
+    
+    public static class ComputeServiceIndicatorsFromChildrenAndMembersSpec 
extends 
ExtensibleEnricherSpec<ComputeServiceIndicatorsFromChildrenAndMembers,ComputeServiceIndicatorsFromChildrenAndMembersSpec>
 {
+        private static final long serialVersionUID = -607444925297963712L;
+        
+        protected ComputeServiceIndicatorsFromChildrenAndMembersSpec() {
+            this(ComputeServiceIndicatorsFromChildrenAndMembers.class);
+        }
+        
+        protected ComputeServiceIndicatorsFromChildrenAndMembersSpec(Class<? 
extends ComputeServiceIndicatorsFromChildrenAndMembers> clazz) {
+            super(clazz);
+        }
+
+        public void addTo(Entity entity) {
+            entity.addEnricher(this);
+        }
+
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkChildrenAndMembers() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, true);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, true);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkMembersOnly() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, true);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, false);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
checkChildrenOnly() {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_MEMBERS, false);
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.FROM_CHILDREN, true);
+            return self();
+        }
+
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
requireUpChildren(QuorumCheck check) {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.UP_QUORUM_CHECK, 
check);
+            return self();
+        }
+        public ComputeServiceIndicatorsFromChildrenAndMembersSpec 
requireRunningChildren(QuorumCheck check) {
+            
configure(ComputeServiceIndicatorsFromChildrenAndMembers.RUNNING_QUORUM_CHECK, 
check);
+            return self();
+        }
+    }
+
+    /** provides the default {@link 
ComputeServiceIndicatorsFromChildrenAndMembers} enricher, 
+     * using the default unique tag ({@link 
ComputeServiceIndicatorsFromChildrenAndMembers#DEFAULT_UNIQUE_TAG}),
+     * configured here to require none on fire, and either no children or at 
least one up child,
+     * the spec can be further configured as appropriate */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildren() {
+        return new ComputeServiceIndicatorsFromChildrenAndMembersSpec()
+            
.uniqueTag(ComputeServiceIndicatorsFromChildrenAndMembers.DEFAULT_UNIQUE_TAG);
+    }
+
+    /** as {@link #newEnricherFromChildren()} but only publishing service 
not-up indicators, 
+     * using a different unique tag ({@link 
ComputeServiceIndicatorsFromChildrenAndMembers#DEFAULT_UNIQUE_TAG_UP}),
+     * listening to children only, ignoring lifecycle/service-state,
+     * and using the same logic 
+     * (viz looking only at children (not members) and requiring either no 
children or at least one child up) by default */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildrenUp() {
+        return newEnricherFromChildren()
+            
.uniqueTag(ComputeServiceIndicatorsFromChildrenAndMembers.DEFAULT_UNIQUE_TAG_UP)
+            .checkChildrenOnly()
+            
.configure(ComputeServiceIndicatorsFromChildrenAndMembers.DERIVE_SERVICE_PROBLEMS,
 false);
+    }
+    
+    /** as {@link #newEnricherFromChildren()} but only publishing service 
problems,
+     * listening to children and members, ignoring service up,
+     * and using the same logic 
+     * (viz looking at children and members and requiring none are on fire) by 
default */
+    public static ComputeServiceIndicatorsFromChildrenAndMembersSpec 
newEnricherFromChildrenState() {
+        return newEnricherFromChildren()
+            
.configure(ComputeServiceIndicatorsFromChildrenAndMembers.DERIVE_SERVICE_NOT_UP,
 false);
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/trait/Changeable.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/trait/Changeable.java 
b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Changeable.java
new file mode 100644
index 0000000..d8fec0e
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Changeable.java
@@ -0,0 +1,35 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.trait;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.sensor.core.BasicNotificationSensor;
+import org.apache.brooklyn.sensor.core.Sensors;
+
+/**
+ * A collection of entities that can change.
+ */
+public interface Changeable {
+
+    AttributeSensor<Integer> GROUP_SIZE = 
Sensors.newIntegerSensor("group.members.count", "Number of members");
+
+    BasicNotificationSensor<Entity> MEMBER_ADDED = new 
BasicNotificationSensor<Entity>(Entity.class, "group.members.added", "Entity 
added to group members");
+    BasicNotificationSensor<Entity> MEMBER_REMOVED = new 
BasicNotificationSensor<Entity>(Entity.class, "group.members.removed", "Entity 
removed from group members");
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/trait/MemberReplaceable.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/trait/MemberReplaceable.java
 
b/core/src/main/java/org/apache/brooklyn/core/entity/trait/MemberReplaceable.java
new file mode 100644
index 0000000..a67b1af
--- /dev/null
+++ 
b/core/src/main/java/org/apache/brooklyn/core/entity/trait/MemberReplaceable.java
@@ -0,0 +1,45 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.trait;
+
+import java.util.NoSuchElementException;
+
+import org.apache.brooklyn.core.annotation.Effector;
+import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.effector.core.MethodEffector;
+import org.apache.brooklyn.entity.group.StopFailedRuntimeException;
+
+public interface MemberReplaceable {
+
+    MethodEffector<String> REPLACE_MEMBER = new 
MethodEffector<String>(MemberReplaceable.class, "replaceMember");
+
+    /**
+     * Replaces the entity with the given ID, if it is a member.
+     * <p>
+     * First adds a new member, then removes this one. 
+     *
+     * @param memberId entity id of a member to be replaced
+     * @return the id of the new entity
+     * @throws NoSuchElementException If entity cannot be resolved, or it is 
not a member
+     * @throws StopFailedRuntimeException If stop failed, after successfully 
starting replacement
+     */
+    @Effector(description="Replaces the entity with the given ID, if it is a 
member; first adds a new member, then removes this one. "+
+            "Returns id of the new entity; or throws exception if couldn't be 
replaced.")
+    String replaceMember(@EffectorParam(name="memberId", description="The 
entity id of a member to be replaced") String memberId);
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/trait/Resizable.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/trait/Resizable.java 
b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Resizable.java
new file mode 100644
index 0000000..ebf37d0
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Resizable.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.trait;
+
+
+import org.apache.brooklyn.core.annotation.Effector;
+import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.effector.core.MethodEffector;
+
+/**
+ * Defines an entity group that can be re-sized dynamically.
+ * <p/>
+ * By invoking the {@link #resize(Integer)} effector, the number of child nodes
+ * can be reduced (by shutting down some of them) or increased (by 
provisioning new entities.)
+ */
+public interface Resizable {
+
+    MethodEffector<Integer> RESIZE = new 
MethodEffector<Integer>(Resizable.class, "resize");
+
+    /**
+     * Grow or shrink this entity to the desired size.
+     *
+     * @param desiredSize the new size of the entity group.
+     * @return the new size of the group.
+     */
+    @Effector(description="Changes the size of the entity (e.g. the number of 
nodes in a cluster)")
+    Integer resize(@EffectorParam(name="desiredSize", description="The new 
size of the cluster") Integer desiredSize);
+
+    /**
+     * @return the current size of the group.
+     */
+    Integer getCurrentSize();
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/trait/Startable.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/brooklyn/core/entity/trait/Startable.java 
b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Startable.java
new file mode 100644
index 0000000..304dba2
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/trait/Startable.java
@@ -0,0 +1,123 @@
+/*
+ * 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 org.apache.brooklyn.core.entity.trait;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.effector.core.MethodEffector;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This interface describes an {@link org.apache.brooklyn.api.entity.Entity} 
that can be started and stopped.
+ *
+ * The {@link Effector}s are {@link #START}, {@link #STOP} and {@link 
#RESTART}. The start effector takes
+ * a collection of {@link Location} objects as an argument which will cause 
the entity to be started or stopped in all
+ * these locations. The other effectors will stop or restart the entity in the 
location(s) it is already running in.
+ */
+public interface Startable {
+
+    
+    AttributeSensor<Boolean> SERVICE_UP = Attributes.SERVICE_UP;
+
+    public static class StartEffectorBody extends EffectorBody<Void> {
+        public static final ConfigKey<Object> LOCATIONS = 
ConfigKeys.newConfigKey(Object.class, "locations",
+            "The location or locations to start in, as a string, a location 
object, a list of strings, "
+            + "or a list of location objects");
+        @Override public Void call(ConfigBag parameters) {
+            parameters.put(LOCATIONS, 
entity().getManagementContext().getLocationRegistry().resolveList(parameters.get(LOCATIONS)));
+            return new MethodEffector<Void>(Startable.class, 
"start").call(entity(), parameters.getAllConfig());
+        }
+    }
+
+    public static class StopEffectorBody extends EffectorBody<Void> {
+        private static final Logger log = 
LoggerFactory.getLogger(Startable.class);
+        
+        @Override public Void call(ConfigBag parameters) {
+            if (!parameters.isEmpty()) {
+                log.warn("Parameters "+parameters+" not supported for call to 
"+entity()+" - "+Tasks.current());
+            }
+            
+            return new MethodEffector<Void>(Startable.class, 
"stop").call(entity(), parameters.getAllConfig());
+        }
+    }
+
+    public static class RestartEffectorBody extends EffectorBody<Void> {
+        private static final Logger log = 
LoggerFactory.getLogger(Startable.class);
+
+        @Override public Void call(ConfigBag parameters) {
+            if (!parameters.isEmpty()) {
+                log.warn("Parameters "+parameters+" not supported for call to 
"+entity()+" - "+Tasks.current());
+            }
+            return new MethodEffector<Void>(Startable.class, 
"restart").call(entity(), parameters.getAllConfig());
+        }
+    }
+
+    org.apache.brooklyn.api.effector.Effector<Void> START = 
Effectors.effector(new MethodEffector<Void>(Startable.class, "start"))
+        // override start to take strings etc
+        .parameter(StartEffectorBody.LOCATIONS)
+        .impl(new StartEffectorBody())
+        .build();
+    
+    org.apache.brooklyn.api.effector.Effector<Void> STOP = 
Effectors.effector(new MethodEffector<Void>(Startable.class, "stop"))
+        .impl(new StopEffectorBody())
+        .build();
+    
+    org.apache.brooklyn.api.effector.Effector<Void> RESTART = 
Effectors.effector(new MethodEffector<Void>(Startable.class, "restart"))
+        .impl(new RestartEffectorBody())
+        .build();
+
+    /**
+     * Start the entity in the given collection of locations.
+     * <p>
+     * Some entities may define custom {@link Effector} implementations which 
support
+     * a richer set of parameters.  See the entity-specific {@link #START} 
effector declaration.
+     */
+    @org.apache.brooklyn.core.annotation.Effector(description="Start the 
process/service represented by an entity")
+    void start(@EffectorParam(name="locations") Collection<? extends Location> 
locations);
+
+    /**
+     * Stop the entity.
+     * <p>
+     * Some entities may define custom {@link Effector} implementations which 
support
+     * a richer set of parameters.  See the entity-specific {@link #STOP} 
effector declaration.
+     */
+    @org.apache.brooklyn.core.annotation.Effector(description="Stop the 
process/service represented by an entity")
+    void stop();
+
+    /**
+     * Restart the entity.
+     * <p>
+     * Some entities may define custom {@link Effector} implementations which 
support
+     * a richer set of parameters.  See the entity-specific {@link #RESTART} 
effector declaration.
+     */
+    @org.apache.brooklyn.core.annotation.Effector(description="Restart the 
process/service represented by an entity")
+    void restart();
+}

Reply via email to