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

[29/62] [abbrv] incubator-brooklyn git commit: rename core’s o.a.b.entity to o.a.b.core.entity

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
new file mode 100644
index 0000000..716f934
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/BrooklynConfigKeys.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+import static org.apache.brooklyn.core.config.ConfigKeys.newBooleanConfigKey;
+import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKey;
+import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKeyWithPrefix;
+import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.server.BrooklynServerConfig;
+import org.apache.brooklyn.sensor.core.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.sensor.core.TemplatedStringAttributeSensorAndConfigKey;
+import org.apache.brooklyn.util.core.internal.ssh.ShellTool;
+import org.apache.brooklyn.util.core.internal.ssh.SshTool;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.base.Preconditions;
+
+/** Commonly used config keys, for use in entities. Similar to {@link Attributes}.
+ * See also {@link BrooklynServerConfig} for config keys for controlling the server. */
+public class BrooklynConfigKeys {
+
+    @Deprecated /** @deprecated since 0.7.0 see BrooklynServerConfig#getPeristenceDir() and BrooklynServerConfigKeys#PERSISTENCE_DIR */
+    public static final ConfigKey<String> BROOKLYN_PERSISTENCE_DIR = BrooklynServerConfig.PERSISTENCE_DIR;
+
+    @Deprecated /** @deprecated since 0.7.0 use BrooklynServerConfig routines */
+    public static final ConfigKey<String> BROOKLYN_DATA_DIR = BrooklynServerConfig.BROOKLYN_DATA_DIR;
+
+    public static final ConfigKey<String> ONBOX_BASE_DIR = newStringConfigKey("onbox.base.dir",
+            "Default base directory on target machines where Brooklyn config data is stored; " +
+            "default depends on the location, either ~/brooklyn-managed-processes or /tmp/brooklyn-${username} on localhost");
+
+    public static final ConfigKey<Boolean> SKIP_ON_BOX_BASE_DIR_RESOLUTION = ConfigKeys.newBooleanConfigKey("onbox.base.dir.skipResolution",
+            "Whether to skip on-box directory resolution (which can require ssh'ing), and just assume the directory exists; can be set on machine or on entity", 
+            false);
+
+    // TODO Rename to VERSION, instead of SUGGESTED_VERSION? And declare as BasicAttributeSensorAndConfigKey?
+    public static final ConfigKey<String> SUGGESTED_VERSION = newStringConfigKey("install.version", "Suggested version");
+
+    public static final ConfigKey<String> INSTALL_UNIQUE_LABEL = ConfigKeys.newStringConfigKey("install.unique_label",
+            "Provides a label which uniquely identifies an installation, used in the computation of the install dir; " +
+            "this should include something readable, and must include a hash of all data which differentiates an installation " +
+            "(e.g. version, plugins, etc), but should be the same where install dirs can be shared to allow for re-use");
+
+    /**
+     * Set this configuration value to true if the entity installation, customization and launch process is to be skipped entirely.
+     * <p>
+     * This is usually because the process or service the entity represents is already present and started, as part of the image
+     * being used. The {@link Startable#SERVICE_UP} attribute will be set in the usual manner.
+     * <p>
+     * If this key is set on a {@link Location} then all entities in that location will be treated in this way. This is useful
+     * when the location is configured with a particular image containing installed and running services.
+     *
+     * @see #ENTITY_RUNNING
+     */
+    public static final ConfigKey<Boolean> SKIP_ENTITY_START = newBooleanConfigKey("entity.started", "Skip the startup process entirely, for running services");
+
+    /**
+     * Set this configuration value to true to skip the entity startup process as with {@link #ENTITY_STARTED} if the process or
+     * service represented by the entity is already running, otherwise proceed normally. This is determined using the driver's
+     * {@code isRunning()} method.
+     * <p>
+     * If this key is set on a {@link Location} then all entities in that location will be treated in this way, again as with {@link #ENTITY_STARTED}.
+     *
+     * @see #ENTITY_STARTED
+     */
+    public static final ConfigKey<Boolean> SKIP_ENTITY_START_IF_RUNNING = newBooleanConfigKey("entity.running", "Skip the startup process entirely, if service already running");
+
+    /**
+     * Set this configuration value to true if the entity installation, customization and launch process is to be skipped entirely.
+     * <p>
+     * This will skip the installation phase of the lifecycle, and move directl;y to customization and launching of the entity.
+     */
+    public static final ConfigKey<Boolean> SKIP_ENTITY_INSTALLATION = newBooleanConfigKey("install.skip", "Skip the driver install commands entirely, for pre-installed software");
+
+    // The implementation in AbstractSoftwareSshDriver runs this command as an SSH command 
+    public static final ConfigKey<String> PRE_INSTALL_COMMAND = ConfigKeys.newStringConfigKey("pre.install.command",
+            "Command to be run prior to the install method being called on the driver");
+    public static final ConfigKey<String> POST_INSTALL_COMMAND = ConfigKeys.newStringConfigKey("post.install.command",
+            "Command to be run after the install method being called on the driver");
+    public static final ConfigKey<String> PRE_LAUNCH_COMMAND = ConfigKeys.newStringConfigKey("pre.launch.command",
+            "Command to be run prior to the launch method being called on the driver");
+    public static final ConfigKey<String> POST_LAUNCH_COMMAND = ConfigKeys.newStringConfigKey("post.launch.command",
+            "Command to be run after the launch method being called on the driver");
+
+    public static final AttributeSensorAndConfigKey<String, String> INSTALL_DIR = new TemplatedStringAttributeSensorAndConfigKey("install.dir", "Directory for this software to be installed in",
+            "${" +
+            "config['"+ONBOX_BASE_DIR.getName()+"']!" +
+            "config['"+BROOKLYN_DATA_DIR.getName()+"']!" +
+            "'/<ERROR>-ONBOX_BASE_DIR-not-set'" +
+            "}" +
+            "/" +
+            "installs/" +
+            // the  var??  tests if it exists, passing value to ?string(if_present,if_absent)
+            // the ! provides a default value afterwards, which is never used, but is required for parsing
+            // when the config key is not available;
+            // thus the below prefers the install.unique_label, but falls back to simple name
+            // plus a version identifier *if* the version is explicitly set
+            "${(config['install.unique_label']??)?string(config['install.unique_label']!'X'," +
+            "(entity.entityType.simpleName)+" +
+            "((config['install.version']??)?string('_'+(config['install.version']!'X'),''))" +
+            ")}");
+
+    public static final AttributeSensorAndConfigKey<String, String> RUN_DIR = new TemplatedStringAttributeSensorAndConfigKey("run.dir", "Directory for this software to be run from",
+            "${" +
+            "config['"+ONBOX_BASE_DIR.getName()+"']!" +
+            "config['"+BROOKLYN_DATA_DIR.getName()+"']!" +
+            "'/<ERROR>-ONBOX_BASE_DIR-not-set'" +
+            "}" +
+            "/" +
+            "apps/${entity.applicationId}/" +
+            "entities/${entity.entityType.simpleName}_" +
+            "${entity.id}");
+
+    public static final AttributeSensorAndConfigKey<String, String> EXPANDED_INSTALL_DIR = new TemplatedStringAttributeSensorAndConfigKey(
+            "expandedinstall.dir", 
+            "Directory for installed artifacts (e.g. expanded dir after unpacking .tgz)", 
+            null);
+
+    /** @deprecated since 0.7.0; use {@link #INSTALL_DIR} */
+    public static final ConfigKey<String> SUGGESTED_INSTALL_DIR = INSTALL_DIR.getConfigKey();
+    /** @deprecated since 0.7.0; use {@link #RUN_DIR} */
+    public static final ConfigKey<String> SUGGESTED_RUN_DIR = RUN_DIR.getConfigKey();
+
+    /*
+     * Intention is to use these with DependentConfiguration.attributeWhenReady, to allow an entity's start
+     * to block until dependents are ready. This is particularly useful when we want to block until a dependent
+     * component is up, but this entity does not care about the dependent component's actual config values.
+     */
+
+    public static final ConfigKey<Boolean> PROVISION_LATCH = newBooleanConfigKey("provision.latch", "Latch for blocking location provision until ready");
+    public static final ConfigKey<Boolean> START_LATCH = newBooleanConfigKey("start.latch", "Latch for blocking start until ready");
+    public static final ConfigKey<Boolean> SETUP_LATCH = newBooleanConfigKey("setup.latch", "Latch for blocking setup until ready");
+    public static final ConfigKey<Boolean> PRE_INSTALL_RESOURCES_LATCH = newBooleanConfigKey("resources.preInstall.latch", "Latch for blocking pre-install resources until ready");
+    public static final ConfigKey<Boolean> INSTALL_RESOURCES_LATCH = newBooleanConfigKey("resources.install.latch", "Latch for blocking install resources until ready");
+    public static final ConfigKey<Boolean> INSTALL_LATCH = newBooleanConfigKey("install.latch", "Latch for blocking install until ready");
+    public static final ConfigKey<Boolean> RUNTIME_RESOURCES_LATCH = newBooleanConfigKey("resources.runtime.latch", "Latch for blocking runtime resources until ready");
+    public static final ConfigKey<Boolean> CUSTOMIZE_LATCH = newBooleanConfigKey("customize.latch", "Latch for blocking customize until ready");
+    public static final ConfigKey<Boolean> LAUNCH_LATCH = newBooleanConfigKey("launch.latch", "Latch for blocking launch until ready");
+
+    public static final ConfigKey<Duration> START_TIMEOUT = newConfigKey(
+            "start.timeout", "Time to wait for process and for SERVICE_UP before failing (in seconds, default 2m)", Duration.seconds(120));
+
+    /* selected properties from SshTool for external public access (e.g. putting on entities) */
+
+    /** Public-facing global config keys for Brooklyn are defined in ConfigKeys, 
+     * and have this prefix pre-prended to the config keys in this class. */
+    public static final String BROOKLYN_SSH_CONFIG_KEY_PREFIX = "brooklyn.ssh.config.";
+
+    // some checks (this line, and a few Preconditions below) that the remote values aren't null, 
+    // because they have some funny circular references
+    static { assert BROOKLYN_SSH_CONFIG_KEY_PREFIX.equals(SshTool.BROOKLYN_CONFIG_KEY_PREFIX) : "static final initializer classload ordering problem"; }
+
+    public static final ConfigKey<String> SSH_TOOL_CLASS = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, 
+            Preconditions.checkNotNull(SshTool.PROP_TOOL_CLASS, "static final initializer classload ordering problem"));
+
+    public static final ConfigKey<String> SSH_CONFIG_HOST = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, SshTool.PROP_HOST);
+    public static final ConfigKey<Integer> SSH_CONFIG_PORT = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, SshTool.PROP_PORT);
+    public static final ConfigKey<String> SSH_CONFIG_USER = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, SshTool.PROP_USER);
+    public static final ConfigKey<String> SSH_CONFIG_PASSWORD = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, SshTool.PROP_PASSWORD);
+
+    public static final ConfigKey<String> SSH_CONFIG_SCRIPT_DIR = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, 
+            Preconditions.checkNotNull(ShellTool.PROP_SCRIPT_DIR, "static final initializer classload ordering problem"));
+    public static final ConfigKey<String> SSH_CONFIG_SCRIPT_HEADER = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, ShellTool.PROP_SCRIPT_HEADER);
+    public static final ConfigKey<String> SSH_CONFIG_DIRECT_HEADER = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, ShellTool.PROP_DIRECT_HEADER);
+    public static final ConfigKey<Boolean> SSH_CONFIG_NO_DELETE_SCRIPT = newConfigKeyWithPrefix(BROOKLYN_SSH_CONFIG_KEY_PREFIX, ShellTool.PROP_NO_DELETE_SCRIPT);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/Entities.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/Entities.java b/core/src/main/java/org/apache/brooklyn/core/entity/Entities.java
new file mode 100644
index 0000000..56393c2
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/Entities.java
@@ -0,0 +1,1108 @@
+/*
+ * 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;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.EntityLocal;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.drivers.EntityDriver;
+import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolver;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.mgmt.ExecutionContext;
+import org.apache.brooklyn.api.mgmt.LocationManager;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+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.api.sensor.Sensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+import org.apache.brooklyn.core.config.Sanitizer;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.entity.trait.StartableMethods;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.mgmt.internal.BrooklynShutdownHooks;
+import org.apache.brooklyn.core.mgmt.internal.EffectorUtils;
+import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+import org.apache.brooklyn.core.mgmt.internal.NonDeploymentManagementContext;
+import org.apache.brooklyn.core.objs.BrooklynObjectInternal;
+import org.apache.brooklyn.core.objs.proxy.EntityProxyImpl;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.location.core.Locations;
+import org.apache.brooklyn.location.core.internal.LocationInternal;
+import org.apache.brooklyn.sensor.core.DependentConfiguration;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.flags.FlagUtils;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.ParallelTask;
+import org.apache.brooklyn.util.core.task.TaskTags;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.core.task.system.SystemTasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Convenience methods for working with entities.
+ * <p>
+ * Also see the various {@code *Methods} classes for traits,
+ * such as {@link StartableMethods} for {@link Startable} implementations.
+ */
+public class Entities {
+
+    private static final Logger log = LoggerFactory.getLogger(Entities.class);
+
+    /**
+     * Names that, if they appear anywhere in an attribute/config/field indicates that it
+     * may be private, so should not be logged etc.
+     * 
+     * @deprecated since 0.7; instead use {@link Sanitizer#SECRET_NAMES}
+     */
+    @Deprecated
+    public static final List<String> SECRET_NAMES = ImmutableList.of(
+            "password",
+            "passwd",
+            "credential",
+            "secret",
+            "private",
+            "access.cert",
+            "access.key");
+
+    /**
+     * Special object used by some setting methods to indicate that a value should be ignored.
+     * <p>
+     * See specific usages of this field to confirm where.
+     */
+    public static final Object UNCHANGED = new Object();
+
+    /**
+     * Special object used by some setting methods to indicate that a value should be removed.
+     * <p>
+     * See specific usages of this field to confirm where.
+     */
+    public static final Object REMOVE = new Object();
+
+    /**
+     * Invokes an {@link Effector} on multiple entities, with the named arguments from the parameters {@link Map}
+     * using the context of the provided {@link Entity}.
+     * <p>
+     * Intended for use only from the callingEntity.
+     * <p>
+     * Returns a {@link ParallelTask} containing the results from each tasks invocation. Calling
+     * {@link java.util.concurrent.Future#get() get()} on this will block until all tasks are complete,
+     * and will throw an exception if any task resulted in an error.
+     *
+     * @return {@link ParallelTask} containing results from each invocation
+     */
+    public static <T> Task<List<T>> invokeEffectorList(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<T> effector, final Map<String,?> parameters) {
+        // formulation is complicated, but it is building up a list of tasks, without blocking on them initially,
+        // but ensuring that when the parallel task is gotten it does block on all of them
+
+        if (entitiesToCall == null){
+            entitiesToCall = ImmutableList.of();
+        }
+
+        List<TaskAdaptable<T>> tasks = Lists.newArrayList();
+
+        for (final Entity entity : entitiesToCall) {
+            tasks.add( Effectors.invocation(entity, effector, parameters) );
+        }
+        ParallelTask<T> invoke = new ParallelTask<T>(
+                MutableMap.of(
+                        "displayName", effector.getName()+" (parallel)",
+                        "description", "Invoking effector \""+effector.getName()+"\" on "+tasks.size()+(tasks.size() == 1 ? " entity" : " entities"),
+                        "tag", BrooklynTaskTags.tagForCallerEntity(callingEntity)),
+                tasks);
+        TaskTags.markInessential(invoke);
+        return DynamicTasks.queueIfPossible(invoke).orSubmitAsync(callingEntity).asTask();
+    }
+
+    public static <T> Task<List<T>> invokeEffectorListWithMap(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<T> effector, final Map<String,?> parameters) {
+        return invokeEffectorList(callingEntity, entitiesToCall, effector, parameters);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> Task<List<T>> invokeEffectorListWithArgs(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<T> effector, Object ...args) {
+        return invokeEffectorListWithMap(callingEntity, entitiesToCall, effector,
+                // putting into a map, unnecessarily, as it ends up being the array again...
+                EffectorUtils.prepareArgsForEffectorAsMapFromArray(effector, args));
+    }
+
+    public static <T> Task<List<T>> invokeEffectorList(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<T> effector) {
+        return invokeEffectorList(callingEntity, entitiesToCall, effector, Collections.<String,Object>emptyMap());
+    }
+
+    public static <T> Task<T> invokeEffector(EntityLocal callingEntity, Entity entityToCall,
+            final Effector<T> effector, final Map<String,?> parameters) {
+        Task<T> t = Effectors.invocation(entityToCall, effector, parameters).asTask();
+        TaskTags.markInessential(t);
+
+        // we pass to callingEntity for consistency above, but in exec-context it should be re-dispatched to targetEntity
+        // reassign t as the return value may be a wrapper, if it is switching execution contexts; see submitInternal's javadoc
+        t = ((EntityInternal)callingEntity).getManagementSupport().getExecutionContext().submit(
+                MutableMap.of("tag", BrooklynTaskTags.tagForCallerEntity(callingEntity)), t);
+
+        if (DynamicTasks.getTaskQueuingContext()!=null) {
+            // include it as a child (in the gui), marked inessential, because the caller is invoking programmatically
+            DynamicTasks.queue(t);
+        }
+
+        return t;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> Task<T> invokeEffectorWithArgs(EntityLocal callingEntity, Entity entityToCall,
+            final Effector<T> effector, Object ...args) {
+        return invokeEffector(callingEntity, entityToCall, effector,
+                EffectorUtils.prepareArgsForEffectorAsMapFromArray(effector, args));
+    }
+
+    public static <T> Task<T> invokeEffector(EntityLocal callingEntity, Entity entityToCall,
+            final Effector<T> effector) {
+        return invokeEffector(callingEntity, entityToCall, effector, Collections.<String,Object>emptyMap());
+    }
+
+    /** Invokes in parallel if multiple, but otherwise invokes the item directly. */
+    public static Task<?> invokeEffector(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<?> effector, final Map<String,?> parameters) {
+        if (Iterables.size(entitiesToCall)==1)
+            return invokeEffector(callingEntity, entitiesToCall.iterator().next(), effector, parameters);
+        else
+            return invokeEffectorList(callingEntity, entitiesToCall, effector, parameters);
+    }
+
+    /** Invokes in parallel if multiple, but otherwise invokes the item directly. */
+    public static Task<?> invokeEffector(EntityLocal callingEntity, Iterable<? extends Entity> entitiesToCall,
+            final Effector<?> effector) {
+        return invokeEffector(callingEntity, entitiesToCall, effector, Collections.<String,Object>emptyMap());
+    }
+
+    /**
+     * @deprecated since 0.7; instead use {@link Sanitizer#IS_SECRET_PREDICATE.apply(Object)}
+     */
+    @Deprecated
+    public static boolean isSecret(String name) {
+        return Sanitizer.IS_SECRET_PREDICATE.apply(name);
+    }
+
+    public static boolean isTrivial(Object v) {
+        if (v instanceof Maybe) {
+            if (!((Maybe<?>)v).isPresent())
+                return true;
+            v = ((Maybe<?>) v).get();
+        }
+
+        return v==null || (v instanceof Map && ((Map<?,?>)v).isEmpty()) ||
+                (v instanceof Collection && ((Collection<?>)v).isEmpty()) ||
+                (v instanceof CharSequence&& ((CharSequence)v).length() == 0);
+    }
+
+    /**
+     * @deprecated since 0.7; instead use {@link Sanitizer#sanitize(ConfigBag)}
+     */
+    @Deprecated
+    public static Map<String,Object> sanitize(ConfigBag input) {
+        return Sanitizer.sanitize(input );
+    }
+
+    /**
+     * @deprecated since 0.7; instead use {@link Sanitizer#sanitize(Map)}
+     */
+    @Deprecated
+    public static <K> Map<K,Object> sanitize(Map<K,?> input) {
+        return Sanitizer.sanitize(input);
+    }
+
+    public static void dumpInfo(Iterable<? extends Entity> entities) {
+        for (Entity e : entities) {
+            dumpInfo(e);
+        }
+    }
+
+    public static void dumpInfo(Entity e) {
+        try {
+            dumpInfo(e, new PrintWriter(System.out), "", "  ");
+        } catch (IOException exc) {
+            // system.out throwing an exception is odd, so don't have IOException on signature
+            throw new RuntimeException(exc);
+        }
+    }
+    public static void dumpInfo(Entity e, Writer out) throws IOException {
+        dumpInfo(e, out, "", "  ");
+    }
+    public static void dumpInfo(Entity e, String currentIndentation, String tab) throws IOException {
+        dumpInfo(e, new PrintWriter(System.out), currentIndentation, tab);
+    }
+    public static void dumpInfo(Entity e, Writer out, String currentIndentation, String tab) throws IOException {
+        out.append(currentIndentation+e.toString()+" "+e.getId()+"\n");
+
+        out.append(currentIndentation+tab+tab+"displayName = "+e.getDisplayName()+"\n");
+
+        out.append(currentIndentation+tab+tab+"locations = "+e.getLocations()+"\n");
+
+        Set<ConfigKey<?>> keys = Sets.newLinkedHashSet( ((EntityInternal)e).getConfigMap().getLocalConfig().keySet() );
+        for (ConfigKey<?> it : sortConfigKeys(keys)) {
+            // use the official config key declared on the type if available
+            // (since the map sometimes contains <object> keys
+            ConfigKey<?> realKey = e.getEntityType().getConfigKey(it.getName());
+            if (realKey!=null) it = realKey;
+
+            Maybe<Object> mv = ((EntityInternal)e).config().getLocalRaw(it);
+            if (!isTrivial(mv)) {
+                Object v = mv.get();
+                out.append(currentIndentation+tab+tab+it.getName());
+                out.append(" = ");
+                if (isSecret(it.getName())) out.append("xxxxxxxx");
+                else if ((v instanceof Task) && ((Task<?>)v).isDone()) {
+                    if (((Task<?>)v).isError()) {
+                        out.append("ERROR in "+v);
+                    } else {
+                        try {
+                            out.append(((Task<?>)v).get() + " (from "+v+")");
+                        } catch (ExecutionException ee) {
+                            throw new IllegalStateException("task "+v+" done and !isError, but threw exception on get", ee);
+                        } catch (InterruptedException ie) {
+                            Thread.currentThread().interrupt();
+                            return;
+                        }
+                    }
+                } else out.append(""+v);
+                out.append("\n");
+            }
+        }
+
+        for (Sensor<?> it : sortSensors(e.getEntityType().getSensors())) {
+            if (it instanceof AttributeSensor) {
+                Object v = e.getAttribute((AttributeSensor<?>)it);
+                if (!isTrivial(v)) {
+                    out.append(currentIndentation+tab+tab+it.getName());
+                    out.append(": ");
+                    if (isSecret(it.getName())) out.append("xxxxxxxx");
+                    else out.append(""+v);
+                    out.append("\n");
+                }
+            }
+        }
+
+        if (e instanceof Group) {
+            StringBuilder members = new StringBuilder();
+            for (Entity it : ((Group)e).getMembers()) {
+                if (members.length()>0) members.append(", ");
+                members.append(it.getId());
+            }
+            out.append(currentIndentation+tab+tab+"Members: "+members.toString()+"\n");
+        }
+
+        if (!e.getPolicies().isEmpty()) {
+            out.append(currentIndentation+tab+tab+"Policies:\n");
+            for (Policy policy : e.getPolicies()) {
+                dumpInfo(policy, out, currentIndentation+tab+tab+tab, tab);
+            }
+        }
+
+        if (!e.getEnrichers().isEmpty()) {
+            out.append(currentIndentation+tab+tab+"Enrichers:\n");
+            for (Enricher enricher : e.getEnrichers()) {
+                dumpInfo(enricher, out, currentIndentation+tab+tab+tab, tab);
+            }
+        }
+
+        for (Entity it : e.getChildren()) {
+            dumpInfo(it, out, currentIndentation+tab, tab);
+        }
+
+        out.flush();
+    }
+
+    public static void dumpInfo(Location loc) {
+        try {
+            dumpInfo(loc, new PrintWriter(System.out), "", "  ");
+        } catch (IOException exc) {
+            // system.out throwing an exception is odd, so don't have IOException on signature
+            throw new RuntimeException(exc);
+        }
+    }
+    public static void dumpInfo(Location loc, Writer out) throws IOException {
+        dumpInfo(loc, out, "", "  ");
+    }
+    public static void dumpInfo(Location loc, String currentIndentation, String tab) throws IOException {
+        dumpInfo(loc, new PrintWriter(System.out), currentIndentation, tab);
+    }
+    @SuppressWarnings("rawtypes")
+    public static void dumpInfo(Location loc, Writer out, String currentIndentation, String tab) throws IOException {
+        out.append(currentIndentation+loc.toString()+"\n");
+
+        for (Object entryO : ((LocationInternal)loc).config().getBag().getAllConfig().entrySet()) {
+            Map.Entry entry = (Map.Entry)entryO;
+            Object keyO = entry.getKey();
+            String key =
+                    keyO instanceof HasConfigKey ? ((HasConfigKey)keyO).getConfigKey().getName() :
+                    keyO instanceof ConfigKey ? ((ConfigKey)keyO).getName() :
+                    keyO == null ? null :
+                    keyO.toString();
+            Object val = entry.getValue();
+            if (!isTrivial(val)) {
+                out.append(currentIndentation+tab+tab+key);
+                out.append(" = ");
+                if (isSecret(key)) out.append("xxxxxxxx");
+                else out.append(""+val);
+                out.append("\n");
+            }
+        }
+
+        for (Map.Entry<String,?> entry : sortMap(FlagUtils.getFieldsWithFlags(loc)).entrySet()) {
+            String key = entry.getKey();
+            Object val = entry.getValue();
+            if (!isTrivial(val)) {
+                out.append(currentIndentation+tab+tab+key);
+                out.append(" = ");
+                if (isSecret(key)) out.append("xxxxxxxx");
+                else out.append(""+val);
+                out.append("\n");
+            }
+        }
+
+        for (Location it : loc.getChildren()) {
+            dumpInfo(it, out, currentIndentation+tab, tab);
+        }
+
+        out.flush();
+    }
+
+    public static void dumpInfo(Enricher enr) {
+        try {
+            dumpInfo(enr, new PrintWriter(System.out), "", "  ");
+        } catch (IOException exc) {
+            // system.out throwing an exception is odd, so don't have IOException on signature
+            throw new RuntimeException(exc);
+        }
+    }
+    public static void dumpInfo(Enricher enr, Writer out) throws IOException {
+        dumpInfo(enr, out, "", "  ");
+    }
+    public static void dumpInfo(Enricher enr, String currentIndentation, String tab) throws IOException {
+        dumpInfo(enr, new PrintWriter(System.out), currentIndentation, tab);
+    }
+    public static void dumpInfo(Enricher enr, Writer out, String currentIndentation, String tab) throws IOException {
+        out.append(currentIndentation+enr.toString()+"\n");
+
+        for (ConfigKey<?> key : sortConfigKeys(enr.getEnricherType().getConfigKeys())) {
+            Maybe<Object> val = ((BrooklynObjectInternal)enr).config().getRaw(key);
+            if (!isTrivial(val)) {
+                out.append(currentIndentation+tab+tab+key);
+                out.append(" = ");
+                if (isSecret(key.getName())) out.append("xxxxxxxx");
+                else out.append(""+val.get());
+                out.append("\n");
+            }
+        }
+
+        out.flush();
+    }
+
+    public static void dumpInfo(Policy pol) {
+        try {
+            dumpInfo(pol, new PrintWriter(System.out), "", "  ");
+        } catch (IOException exc) {
+            // system.out throwing an exception is odd, so don't have IOException on signature
+            throw new RuntimeException(exc);
+        }
+    }
+    public static void dumpInfo(Policy pol, Writer out) throws IOException {
+        dumpInfo(pol, out, "", "  ");
+    }
+    public static void dumpInfo(Policy pol, String currentIndentation, String tab) throws IOException {
+        dumpInfo(pol, new PrintWriter(System.out), currentIndentation, tab);
+    }
+    public static void dumpInfo(Policy pol, Writer out, String currentIndentation, String tab) throws IOException {
+        out.append(currentIndentation+pol.toString()+"\n");
+
+        for (ConfigKey<?> key : sortConfigKeys(pol.getPolicyType().getConfigKeys())) {
+            Maybe<Object> val = ((BrooklynObjectInternal)pol).config().getRaw(key);
+            if (!isTrivial(val)) {
+                out.append(currentIndentation+tab+tab+key);
+                out.append(" = ");
+                if (isSecret(key.getName())) out.append("xxxxxxxx");
+                else out.append(""+val.get());
+                out.append("\n");
+            }
+        }
+
+        out.flush();
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static List<Sensor<?>> sortSensors(Set<Sensor<?>> sensors) {
+        List result = new ArrayList(sensors);
+        Collections.sort(result, new Comparator<Sensor>() {
+                    @Override
+                    public int compare(Sensor arg0, Sensor arg1) {
+                        return arg0.getName().compareTo(arg1.getName());
+                    }
+
+        });
+        return result;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public static List<ConfigKey<?>> sortConfigKeys(Set<ConfigKey<?>> configs) {
+        List result = new ArrayList(configs);
+        Collections.sort(result, new Comparator<ConfigKey>() {
+                    @Override
+                    public int compare(ConfigKey arg0, ConfigKey arg1) {
+                        return arg0.getName().compareTo(arg1.getName());
+                    }
+
+        });
+        return result;
+    }
+
+    public static <T> Map<String, T> sortMap(Map<String, T> map) {
+        Map<String,T> result = Maps.newLinkedHashMap();
+        List<String> order = Lists.newArrayList(map.keySet());
+        Collections.sort(order, String.CASE_INSENSITIVE_ORDER);
+
+        for (String key : order) {
+            result.put(key, map.get(key));
+        }
+        return result;
+    }
+
+    /**
+     * Returns true if the given descendant includes the given ancestor in its chain.
+     * Does <i>NOT</i> count a node as its ancestor.
+     */
+    public static boolean isAncestor(Entity descendant, Entity potentialAncestor) {
+        Entity ancestor = descendant.getParent();
+        while (ancestor != null) {
+            if (ancestor.equals(potentialAncestor)) return true;
+            ancestor = ancestor.getParent();
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether the descendants of the given ancestor contains the given potentialDescendant.
+     * <p>
+     * In this test, unlike in {@link #descendants(Entity)}, an entity is not counted as a descendant.
+     * note, it is usually preferred to use isAncestor() and swap the order, it is a cheaper method.
+     */
+    public static boolean isDescendant(Entity ancestor, Entity potentialDescendant) {
+        Set<Entity> inspected = Sets.newLinkedHashSet();
+        Stack<Entity> toinspect = new Stack<Entity>();
+        toinspect.add(ancestor);
+
+        while (!toinspect.isEmpty()) {
+            Entity e = toinspect.pop();
+            if (e.getChildren().contains(potentialDescendant)) {
+                return true;
+            }
+            inspected.add(e);
+            toinspect.addAll(e.getChildren());
+            toinspect.removeAll(inspected);
+        }
+
+        return false;
+    }
+
+    /**
+     * Return all descendants of given entity matching the given predicate and optionally the entity itself.
+     *
+     * @see {@link EntityPredicates} for useful second arguments.
+     */
+    public static Iterable<Entity> descendants(Entity root, Predicate<? super Entity> matching, boolean includeSelf) {
+        Iterable<Entity> descs = Iterables.concat(Iterables.transform(root.getChildren(), new Function<Entity,Iterable<Entity>>() {
+            @Override
+            public Iterable<Entity> apply(Entity input) {
+                return descendants(input);
+            }
+        }));
+        return Iterables.filter(Iterables.concat(descs, Collections.singleton(root)), matching);
+    }
+
+    /**
+     * Returns the entity  matching the given predicate
+     *
+     * @see #descendants(Entity, Predicate, boolean)
+     */
+    public static Iterable<Entity> descendants(Entity root, Predicate<? super Entity> matching) {
+        return descendants(root, matching, true);
+    }
+
+    /**
+     * Returns the entity, its children, and all its children, and so on.
+     *
+     * @see #descendants(Entity, Predicate, boolean)
+     */
+    public static Iterable<Entity> descendants(Entity root) {
+        return descendants(root, Predicates.alwaysTrue(), true);
+    }
+
+    /**
+     * Return all descendants of given entity of the given type, potentially including the given root.
+     *
+     * @see #descendants(Entity)
+     * @see Iterables#filter(Iterable, Class)
+     */
+    public static <T extends Entity> Iterable<T> descendants(Entity root, Class<T> ofType) {
+        return Iterables.filter(descendants(root), ofType);
+    }
+
+    /** Returns the entity, its parent, its parent, and so on. */
+    public static Iterable<Entity> ancestors(final Entity root) {
+        return new Iterable<Entity>() {
+            @Override
+            public Iterator<Entity> iterator() {
+                return new Iterator<Entity>() {
+                    Entity next = root;
+                    @Override
+                    public boolean hasNext() {
+                        return next!=null;
+                    }
+                    @Override
+                    public Entity next() {
+                        Entity result = next;
+                        next = next.getParent();
+                        return result;
+                    }
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+        };
+    }
+
+    /**
+     * Registers a {@link BrooklynShutdownHooks#invokeStopOnShutdown(Entity)} to shutdown this entity when the JVM exits.
+     * (Convenience method located in this class for easy access.)
+     */
+    public static void invokeStopOnShutdown(Entity entity) {
+        BrooklynShutdownHooks.invokeStopOnShutdown(entity);
+    }
+
+    /** convenience for starting an entity, esp a new Startable instance which has been created dynamically
+     * (after the application is started) */
+    public static void start(Entity e, Collection<? extends Location> locations) {
+        if (!isManaged(e) && !manage(e)) {
+            log.warn("Using discouraged mechanism to start management -- Entities.start(Application, Locations) -- caller should create and use the preferred management context");
+            startManagement(e);
+        }
+        if (e instanceof Startable) Entities.invokeEffector((EntityLocal)e, e, Startable.START,
+                MutableMap.of("locations", locations)).getUnchecked();
+    }
+
+    /**
+     * Attempts to stop, destroy, and unmanage the given entity.
+     * <p>
+     * Actual actions performed will depend on the entity type and its current state.
+     */
+    public static void destroy(Entity e) {
+        if (isManaged(e)) {
+            if (isReadOnly(e)) {
+                unmanage(e);
+                log.debug("destroyed and unmanaged read-only copy of "+e);
+            } else {
+                if (e instanceof Startable) Entities.invokeEffector((EntityLocal)e, e, Startable.STOP).getUnchecked();
+                
+                // if destroying gracefully we might also want to do this (currently gets done by GC after unmanage,
+                // which is good enough for leaks, but not sure if that's ideal for subscriptions etc)
+//                ((LocalEntityManager)e.getApplication().getManagementContext().getEntityManager()).stopTasks(e, null);
+                
+                if (e instanceof EntityInternal) ((EntityInternal)e).destroy();
+                
+                unmanage(e);
+                
+                log.debug("destroyed and unmanaged "+e+"; mgmt now "+
+                    (e.getApplicationId()==null ? "(no app)" : e.getApplication().getManagementContext())+" - managed? "+isManaged(e));
+            }
+        } else {
+            log.debug("skipping destroy of "+e+": not managed");
+        }
+    }
+
+    /** Same as {@link #destroy(Entity)} but catching all errors. */
+    public static void destroyCatching(Entity entity) {
+        try {
+            destroy(entity);
+        } catch (Exception e) {
+            log.warn("ERROR destroying "+entity+" (ignoring): "+e, e);
+            Exceptions.propagateIfFatal(e);
+        }
+    }
+
+    /** Destroys the given location. */
+    public static void destroy(Location loc) {
+        // TODO unmanage the location, if possible?
+        if (loc instanceof Closeable) {
+            Streams.closeQuietly((Closeable)loc);
+            log.debug("closed "+loc);
+        }
+    }
+
+    /** Same as {@link #destroy(Location)} but catching all errors. */
+    public static void destroyCatching(Location loc) {
+        try {
+            destroy(loc);
+        } catch (Exception e) {
+            log.warn("ERROR destroying "+loc+" (ignoring): "+e, e);
+            Exceptions.propagateIfFatal(e);
+        }
+    }
+
+    /**
+     * Stops, destroys, and unmanages all apps in the given context, and then terminates the management context.
+     * 
+     * Apps will be stopped+destroyed+unmanaged concurrently, waiting for all to complete.
+     */
+    public static void destroyAll(final ManagementContext mgmt) {
+        if (mgmt instanceof NonDeploymentManagementContext) {
+            // log here because it is easy for tests to destroyAll(app.getMgmtContext())
+            // which will *not* destroy the mgmt context if the app has been stopped!
+            log.warn("Entities.destroyAll invoked on non-deployment "+mgmt+" - not likely to have much effect! " +
+                    "(This usually means the mgmt context has been taken from an entity that has been destroyed. " +
+                    "To destroy other things on the management context ensure you keep a handle to the context " +
+                    "before the entity is destroyed, such as by creating the management context first.)");
+        }
+        if (!mgmt.isRunning()) return;
+        
+        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        List<ListenableFuture<?>> futures = Lists.newArrayList();
+        final AtomicReference<Exception> error = Atomics.newReference();
+        try {
+            log.debug("destroying all apps in "+mgmt+": "+mgmt.getApplications());
+            for (final Application app: mgmt.getApplications()) {
+                futures.add(executor.submit(new Runnable() {
+                    public void run() {
+                        log.debug("destroying app "+app+" (managed? "+isManaged(app)+"; mgmt is "+mgmt+")");
+                        try {
+                            destroy(app);
+                            log.debug("destroyed app "+app+"; mgmt now "+mgmt);
+                        } catch (Exception e) {
+                            log.warn("problems destroying app "+app+" (mgmt now "+mgmt+", will rethrow at least one exception): "+e);
+                            error.compareAndSet(null, e);
+                        }
+                    }}));
+            }
+            Futures.allAsList(futures).get();
+            
+            for (Location loc : mgmt.getLocationManager().getLocations()) {
+                destroyCatching(loc);
+            }
+            if (mgmt instanceof ManagementContextInternal) {
+                ((ManagementContextInternal)mgmt).terminate();
+            }
+            if (error.get() != null) throw Exceptions.propagate(error.get());
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        } catch (ExecutionException e) {
+            throw Exceptions.propagate(e);
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    /** Same as {@link #destroyAll(ManagementContext)} but catching all errors */
+    public static void destroyAllCatching(ManagementContext mgmt) {
+        try {
+            destroyAll(mgmt);
+        } catch (Exception e) {
+            log.warn("ERROR destroying "+mgmt+" (ignoring): "+e, e);
+            Exceptions.propagateIfFatal(e);
+        }
+    }
+
+    public static boolean isManaged(Entity e) {
+        return ((EntityInternal)e).getManagementSupport().isDeployed() && ((EntityInternal)e).getManagementContext().isRunning();
+    }
+
+    public static boolean isNoLongerManaged(Entity e) {
+        return ((EntityInternal)e).getManagementSupport().isNoLongerManaged();
+    }
+
+    /** as {@link EntityManagerInternal#isReadOnly(Entity)} */
+    @Beta
+    public static Boolean isReadOnly(Entity e) {
+        return ((EntityInternal)e).getManagementSupport().isReadOnly();
+    }
+
+    /** Unwraps a proxy to retrieve the real item, if available.
+     * <p>
+     * Only intended for use in tests and occasional internal usage, e.g. persistence.
+     * For normal operations, callers should ensure the method is available on an interface and accessed via the proxy. */
+    @Beta @VisibleForTesting
+    public static AbstractEntity deproxy(Entity e) {
+        if (!(Proxy.isProxyClass(e.getClass()))) {
+            log.warn("Attempt to deproxy non-proxy "+e, new Throwable("Location of attempt to deproxy non-proxy "+e));
+            return (AbstractEntity) e;
+        }
+        return (AbstractEntity) ((EntityProxyImpl)Proxy.getInvocationHandler(e)).getDelegate();
+    }
+    
+    /** 
+     * Returns the proxy form (if available) of the entity. If already a proxy, returns unmodified.
+     * 
+     * If null is passed in, then null is returned.
+     * 
+     * For legacy entities (that did not use {@link EntitySpec} or YAML for creation), the
+     * proxy may not be avilable; in which case the concrete class passed in will be returned.
+     */
+    @Beta
+    @SuppressWarnings("unchecked")
+    public static <T extends Entity> T proxy(T e) {
+        return (e == null) ? null : e instanceof Proxy ? e : (T) ((AbstractEntity)e).getProxyIfAvailable();
+    }
+    
+    /**
+     * Brings this entity under management only if its ancestor is managed.
+     * <p>
+     * Returns true if successful, otherwise returns false in the expectation that the ancestor
+     * will become managed, or throws exception if it has no parent or a non-application root.
+     *
+     * @throws IllegalStateException if {@literal e} is an {@link Application}.
+     * @see #startManagement(Entity)
+     */
+    public static boolean manage(Entity e) {
+        Entity o = e.getParent();
+        Entity eum = e; // Highest unmanaged ancestor
+        if (o==null) throw new IllegalArgumentException("Can't manage "+e+" because it is an orphan");
+        while (o.getParent()!=null) {
+            if (!isManaged(o)) eum = o;
+            o = o.getParent();
+        }
+        if (isManaged(o)) {
+            ((EntityInternal)o).getManagementContext().getEntityManager().manage(eum);
+            return true;
+        }
+        if (!(o instanceof Application)) {
+            throw new IllegalStateException("Can't manage "+e+" because it is not rooted at an application");
+        }
+        return false;
+    }
+
+    /**
+     * Brings this entity under management, creating a local management context if necessary,
+     * assuming root is an application.
+     * <p>
+     * Returns existing management context if there is one (non-deployment) or a new local management
+     * context if not, or throws an exception if root is not an application. Callers are recommended
+     * to use {@link #manage(Entity)} instead unless they know a plain-vanilla non-root management
+     * context is sufficient e.g. in tests.
+     * <p>
+     * <b>NOTE</b> This method may change, but is provided as a stop-gap to prevent ad-hoc things
+     * being done in the code which are even more likely to break!
+     */
+    @Beta
+    public static ManagementContext startManagement(Entity e) {
+        Entity o = e;
+        Entity eum = e; // Highest unmanaged ancestor
+        while (o.getParent()!=null) {
+            if (!isManaged(o)) eum = o;
+            o = o.getParent();
+        }
+        if (isManaged(o)) {
+            ManagementContext mgmt = ((EntityInternal)o).getManagementContext();
+            mgmt.getEntityManager().manage(eum);
+            return mgmt;
+        }
+        if (!(o instanceof Application))
+            throw new IllegalStateException("Can't manage "+e+" because it is not rooted at an application");
+        
+        log.warn("Deprecated invocation of startManagement for "+e+" without a management context present; "
+            + "a new local management context is being created! (Not recommended unless you really know what you are doing.)");
+        ManagementContext mgmt = new LocalManagementContext();
+        mgmt.getEntityManager().manage(o);
+        return mgmt;
+    }
+
+    /**
+     * Starts managing the given (unmanaged) app, using the given management context.
+     *
+     * @see #startManagement(Entity)
+     */
+    public static ManagementContext startManagement(Application app, ManagementContext mgmt) {
+        if (isManaged(app)) {
+            throw new IllegalStateException("Application "+app+" is already managed, so can't set brooklyn properties");
+        }
+        mgmt.getEntityManager().manage(app);
+        return mgmt;
+    }
+
+    /**
+     * Starts managing the given (unmanaged) app, setting the given brooklyn properties on the new
+     * management context.
+     *
+     * @see #startManagement(Entity)
+     */
+    public static ManagementContext startManagement(Application app, BrooklynProperties props) {
+        if (isManaged(app)) {
+            throw new IllegalStateException("Application "+app+" is already managed, so can't set brooklyn properties");
+        }
+        ManagementContext mgmt = new LocalManagementContext(props);
+        mgmt.getEntityManager().manage(app);
+        return mgmt;
+    }
+
+    public static ManagementContext newManagementContext() {
+        return new LocalManagementContext();
+    }
+
+    public static ManagementContext newManagementContext(BrooklynProperties props) {
+        return new LocalManagementContext(props);
+    }
+
+    public static ManagementContext newManagementContext(Map<?,?> props) {
+        return new LocalManagementContext( BrooklynProperties.Factory.newEmpty().addFromMap(props));
+    }
+
+    public static ManagementContext getManagementContext(Entity entity) {
+        return ((EntityInternal) entity).getManagementContext();
+    }
+
+    public static void unmanage(Entity entity) {
+        if (((EntityInternal)entity).getManagementSupport().isDeployed()) {
+            ((EntityInternal)entity).getManagementContext().getEntityManager().unmanage(entity);
+        }
+    }
+
+    public static DownloadResolver newDownloader(EntityDriver driver) {
+        return newDownloader(driver, ImmutableMap.<String,Object>of());
+    }
+
+    public static DownloadResolver newDownloader(EntityDriver driver, Map<String,?> properties) {
+        EntityInternal internal = (EntityInternal) driver.getEntity();
+        return internal.getManagementContext().getEntityDownloadsManager().newDownloader(driver, properties);
+    }
+
+    public static DownloadResolver newDownloader(EntityDriver driver, String addon) {
+        return newDownloader(driver, addon, ImmutableMap.<String,Object>of());
+    }
+
+    public static DownloadResolver newDownloader(EntityDriver driver, String addon, Map<String,?> properties) {
+        EntityInternal internal = (EntityInternal) driver.getEntity();
+        return internal.getManagementContext().getEntityDownloadsManager().newDownloader(driver, addon, properties);
+    }
+
+    public static <T> Supplier<T> attributeSupplier(Entity entity, AttributeSensor<T> sensor) {
+        return EntityAndAttribute.supplier(entity, sensor);
+    }
+
+    public static <T> Supplier<T> attributeSupplier(EntityAndAttribute<T> tuple) { return tuple; }
+
+    public static <T> Supplier<T> attributeSupplierWhenReady(EntityAndAttribute<T> tuple) {
+        return attributeSupplierWhenReady(tuple.getEntity(), tuple.getAttribute());
+    }
+
+    @SuppressWarnings({ "unchecked", "serial" })
+    public static <T> Supplier<T> attributeSupplierWhenReady(final Entity entity, final AttributeSensor<T> sensor) {
+        final Task<T> task = DependentConfiguration.attributeWhenReady(entity, sensor);
+        return new Supplier<T>() {
+            @Override public T get() {
+                try {
+                    TypeToken<T> type = new TypeToken<T>(sensor.getType()) {};
+                    return Tasks.resolveValue(task, (Class<T>) type.getRawType(), ((EntityInternal) entity).getExecutionContext(), "attributeSupplierWhenReady");
+                } catch (Exception e) {
+                    throw Exceptions.propagate(e);
+                }
+            }
+        };
+    }
+
+    /**
+     * @since 0.6.0 Added only for backwards compatibility, where locations are being created directly.
+     * @deprecated in 0.6.0; use {@link LocationManager#createLocation(LocationSpec)} instead
+     */
+    @Deprecated
+    public static void manage(Location loc, ManagementContext managementContext) {
+        Locations.manage(loc, managementContext);
+    }
+
+    /** Fails-fast if value of the given key is null or unresolveable. */
+    public static String getRequiredUrlConfig(Entity entity, ConfigKey<String> urlKey) {
+        String url = entity.getConfig(urlKey);
+        Preconditions.checkNotNull(url, "Key %s on %s should not be null", urlKey, entity);
+        if (!ResourceUtils.create(entity).doesUrlExist(url)) {
+            throw new IllegalStateException(String.format("Key %s on %s contains unavailable URL %s", urlKey, entity, url));
+        }
+        return url;
+    }
+
+    /** @see #getRequiredUrlConfig(Entity, ConfigKey) */
+    public static String getRequiredUrlConfig(Entity entity, HasConfigKey<String> urlKey) {
+        return getRequiredUrlConfig(entity, urlKey.getConfigKey());
+    }
+
+    /** Fails-fast if value of the given URL is null or unresolveable. */
+    public static String checkRequiredUrl(Entity entity, String url) {
+        Preconditions.checkNotNull(url, "url");
+        if (!ResourceUtils.create(entity).doesUrlExist(url)) {
+            throw new IllegalStateException(String.format("URL %s on %s is unavailable", url, entity));
+        }
+        return url;
+    }
+
+    /**
+     * Submits a {@link TaskFactory} to construct its task at the entity (in a precursor task) and then to submit it.
+     * <p>
+     * Important if task construction relies on an entity being in scope (in tags, via {@link BrooklynTaskTags})
+     */
+    public static <T extends TaskAdaptable<?>> T submit(final Entity entity, final TaskFactory<T> taskFactory) {
+        // TODO it is messy to have to do this, but not sure there is a cleaner way :(
+        final Semaphore s = new Semaphore(0);
+        final AtomicReference<T> result = new AtomicReference<T>();
+        final ExecutionContext executionContext = ((EntityInternal)entity).getManagementSupport().getExecutionContext();
+        executionContext.execute(new Runnable() {
+            // TODO could give this task a name, like "create task from factory"
+            @Override
+            public void run() {
+                T t = taskFactory.newTask();
+                result.set(t);
+                s.release();
+            }
+        });
+        try {
+            s.acquire();
+        } catch (InterruptedException e) {
+            throw Exceptions.propagate(e);
+        }
+        executionContext.submit(result.get().asTask());
+        return result.get();
+    }
+
+    /**
+     * Submits a task to run at the entity.
+     * 
+     * @return the task passed in, for fluency
+     */
+    public static <T extends TaskAdaptable<?>> T submit(final Entity entity, final T task) {
+        final ExecutionContext executionContext = ((EntityInternal)entity).getManagementSupport().getExecutionContext();
+        executionContext.submit(task.asTask());
+        return task;
+    }
+
+    /** Logs a warning if an entity has a value for a config key. */
+    public static void warnOnIgnoringConfig(Entity entity, ConfigKey<?> key) {
+        if (entity.getConfigRaw(key, true).isPresentAndNonNull())
+            log.warn("Ignoring "+key+" set on "+entity+" ("+entity.getConfig(key)+")");
+    }
+
+    /** Waits until {@link Startable#SERVICE_UP} returns true. */
+    public static void waitForServiceUp(final Entity entity, Duration timeout) {
+        String description = "Waiting for SERVICE_UP on "+entity;
+        Tasks.setBlockingDetails(description);
+        try {
+            if (!Repeater.create(description).limitTimeTo(timeout)
+                    .rethrowException().backoffTo(Duration.ONE_SECOND)
+                    .until(new Callable<Boolean>() {
+                        public Boolean call() {
+                            return Boolean.TRUE.equals(entity.getAttribute(Startable.SERVICE_UP));
+                        }})
+                    .run()) {
+                throw new IllegalStateException("Timeout waiting for SERVICE_UP from "+entity);
+            }
+        } finally {
+            Tasks.resetBlockingDetails();
+        }
+        log.debug("Detected SERVICE_UP for software {}", entity);
+    }
+    public static void waitForServiceUp(final Entity entity, long duration, TimeUnit units) {
+        waitForServiceUp(entity, Duration.of(duration, units));
+    }
+    public static void waitForServiceUp(final Entity entity) {
+        Duration timeout = entity.getConfig(BrooklynConfigKeys.START_TIMEOUT);
+        waitForServiceUp(entity, timeout);
+    }
+
+    /**
+     * Convenience for creating and submitted a given shell command against the given mgmt context,
+     * primarily intended for use in the groovy GUI console.
+     */
+    @Beta
+    public static ProcessTaskWrapper<Integer> shell(ManagementContext mgmt, String command) {
+        ProcessTaskWrapper<Integer> t = SystemTasks.exec(command).newTask();
+        mgmt.getServerExecutionContext().submit(t).getUnchecked();
+        System.out.println(t.getStdout());
+        System.err.println(t.getStderr());
+        return t;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/EntityAdjuncts.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/EntityAdjuncts.java b/core/src/main/java/org/apache/brooklyn/core/entity/EntityAdjuncts.java
new file mode 100644
index 0000000..f5a6633
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/EntityAdjuncts.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.objs.EntityAdjunct;
+import org.apache.brooklyn.api.sensor.Enricher;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ComputeServiceIndicatorsFromChildrenAndMembers;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ComputeServiceState;
+import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.guava.Maybe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Convenience methods for working with entity adjunts.
+ */
+public class EntityAdjuncts {
+
+    public static <T extends EntityAdjunct> Maybe<T> tryFindWithUniqueTag(Iterable<T> adjuncts, Object tag) {
+        Preconditions.checkNotNull(tag, "tag");
+        for (T adjunct: adjuncts)
+            if (tag.equals(adjunct.getUniqueTag())) 
+                return Maybe.of(adjunct);
+        return Maybe.absent("Not found with tag "+tag);
+    }
+    
+    public static final List<String> SYSTEM_ENRICHER_UNIQUE_TAGS = ImmutableList.of(
+        ServiceNotUpLogic.DEFAULT_ENRICHER_UNIQUE_TAG,
+        ComputeServiceState.DEFAULT_ENRICHER_UNIQUE_TAG,
+        ComputeServiceIndicatorsFromChildrenAndMembers.DEFAULT_UNIQUE_TAG,
+        ComputeServiceIndicatorsFromChildrenAndMembers.DEFAULT_UNIQUE_TAG_UP);
+    
+    public static List<Enricher> getNonSystemEnrichers(Entity entity) {
+        List<Enricher> result = MutableList.copyOf(entity.getEnrichers());
+        Iterator<Enricher> ri = result.iterator();
+        while (ri.hasNext()) {
+            if (isSystemEnricher(ri.next())) ri.remove();
+        }
+        return result;
+    }
+
+    public static boolean isSystemEnricher(Enricher enr) {
+        if (enr.getUniqueTag()==null) return false;
+        if (SYSTEM_ENRICHER_UNIQUE_TAGS.contains(enr.getUniqueTag())) return true;
+        return false;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/EntityAndAttribute.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/EntityAndAttribute.java b/core/src/main/java/org/apache/brooklyn/core/entity/EntityAndAttribute.java
new file mode 100644
index 0000000..e1ae639
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/EntityAndAttribute.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Supplier;
+
+/**
+ * A tuple containing an {@link Entity} and an {@link AttributeSensor}, which is assumed to be present on the entity.
+ * <p>
+ * Allows retrieval of the attribute {@link #getValue() value} or can be used instead where a {@link Supplier} for
+ * the attribute value is required.
+ */
+public class EntityAndAttribute<T> implements Supplier<T> {
+
+    private final Entity entity;
+    private final AttributeSensor<T> attribute;
+
+    public static <T> EntityAndAttribute<T> create(Entity entity, AttributeSensor<T> attribute) {
+        return new EntityAndAttribute<T>(entity, attribute);
+    }
+
+    /**
+     * @deprecated since 0.7.0; use {@link #create(Entity, AttributeSensor)}; this does not relate to {@link Supplier}
+     */
+    public static <T> EntityAndAttribute<T> supplier(Entity entity, AttributeSensor<T> attribute) {
+        return create(entity, attribute);
+    }
+
+    public EntityAndAttribute(Entity entity, AttributeSensor<T> attribute) {
+        this.entity = checkNotNull(entity, "entity");
+        this.attribute = checkNotNull(attribute, "attribute");
+    }
+
+    public Entity getEntity() {
+        return entity;
+    }
+
+    public AttributeSensor<T> getAttribute() {
+        return attribute;
+    }
+
+    public T getValue() {
+        return entity.getAttribute(attribute);
+    }
+
+    public void setValue(T val) {
+        ((EntityLocal)entity).setAttribute(attribute, val);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * Returns the current value of the {@link #getAttribute() attribute} on the {@link #getEntity() entity}.
+     *
+     * @see #getValue()
+     */
+    @Override
+    public T get() {
+        return getValue();
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("entity", entity)
+                .add("attribute", attribute)
+                .toString();
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(entity, attribute);
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) return false;
+        if (!(o instanceof EntityAndAttribute)) return false;
+        EntityAndAttribute<?> that = (EntityAndAttribute<?>) o;
+        return Objects.equal(this.entity, that.entity) &&
+                Objects.equal(this.attribute, that.attribute);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c27cf1d0/core/src/main/java/org/apache/brooklyn/core/entity/EntityDynamicType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/EntityDynamicType.java b/core/src/main/java/org/apache/brooklyn/core/entity/EntityDynamicType.java
new file mode 100644
index 0000000..0db0d46
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/entity/EntityDynamicType.java
@@ -0,0 +1,339 @@
+/*
+ * 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;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityType;
+import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
+import org.apache.brooklyn.core.objs.BrooklynDynamicType;
+import org.apache.brooklyn.effector.core.EffectorAndBody;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.EffectorWithBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.effector.core.MethodEffector;
+import org.apache.brooklyn.effector.core.EffectorTasks.EffectorBodyTaskFactory;
+import org.apache.brooklyn.effector.core.EffectorTasks.EffectorTaskFactory;
+import org.apache.brooklyn.util.javalang.Reflections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Maps;
+
+/** This is the actual type of an entity instance at runtime,
+ * which can change from the static {@link EntityType}, and can change over time;
+ * for this reason it does *not* implement EntityType, but 
+ * callers can call {@link #getSnapshot()} to get a snapshot such instance  
+ */
+public class EntityDynamicType extends BrooklynDynamicType<Entity, AbstractEntity> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(EntityDynamicType.class);
+
+    /** 
+     * Effectors on this entity, by name.
+     */
+    // TODO support overloading; requires not using a map keyed off method name.
+    private final Map<String, Effector<?>> effectors = new ConcurrentHashMap<String, Effector<?>>();
+
+    /** 
+     * Map of sensors on this entity, by name.
+     */
+    private final ConcurrentMap<String,Sensor<?>> sensors = new ConcurrentHashMap<String, Sensor<?>>();
+
+    public EntityDynamicType(AbstractEntity entity) {
+        this(entity.getClass(), entity);
+    }
+    public EntityDynamicType(Class<? extends Entity> clazz) {
+        this(clazz, null);
+    }
+    private EntityDynamicType(Class<? extends Entity> clazz, AbstractEntity entity) {
+        super(clazz, entity);
+        String id = entity==null ? clazz.getName() : entity.getId();
+        
+        effectors.putAll(findEffectors(clazz, null));
+        if (LOG.isTraceEnabled())
+            LOG.trace("Entity {} effectors: {}", id, Joiner.on(", ").join(effectors.keySet()));
+        
+        sensors.putAll(findSensors(clazz, null));
+        if (LOG.isTraceEnabled())
+            LOG.trace("Entity {} sensors: {}", id, Joiner.on(", ").join(sensors.keySet()));
+        
+        refreshSnapshot();
+    }
+    
+    /**
+     * @deprecated since 0.7; unused code; instead use {@link #getBrooklynClass()}
+     */
+    @Deprecated
+    public Class<? extends Entity> getEntityClass() {
+        return super.getBrooklynClass();
+    }
+    
+    public EntityType getSnapshot() {
+        return (EntityType) super.getSnapshot();
+    }
+
+    // --------------------------------------------------
+    
+    /**
+     * @return the effector with the given name, or null if not found
+     */
+    public Effector<?> getEffector(String name) {
+        return effectors.get(name);
+    }
+    
+    /**
+     * Effectors available on this entity.
+     */
+    public Map<String,Effector<?>> getEffectors() {
+        return Collections.unmodifiableMap(effectors);
+    }
+    
+    /**
+     * Adds the given {@link Effector} to this entity.
+     */
+    @Beta
+    public void addEffector(Effector<?> newEffector) {
+        Effector<?> oldEffector = effectors.put(newEffector.getName(), newEffector);
+        invalidateSnapshot();
+        if (oldEffector!=null)
+            instance.emit(AbstractEntity.EFFECTOR_CHANGED, newEffector.getName());
+        else
+            instance.emit(AbstractEntity.EFFECTOR_ADDED, newEffector.getName());
+    }
+
+    /**
+     * Adds an effector with an explicit body to this entity.
+     */
+    @Beta
+    public <T> void addEffector(Effector<T> effector, EffectorTaskFactory<T> body) {
+        addEffector(new EffectorAndBody<T>(effector, body));
+    }
+
+    /**
+     * Adds an effector with an explicit body to this entity.
+     */
+    @Beta
+    public <T> void addEffector(Effector<T> effector, EffectorBody<T> body) {
+        addEffector(effector, new EffectorBodyTaskFactory<T>(body));
+    }
+
+    /**
+     * Removes the given {@link Effector} from this entity.
+     * <p>
+     * Note that if the argument is an instance of {@link EffectorWithBody} it will
+     * still be possible to invoke the effector on the entity by calling
+     * <code>entity.invoke(effector, argumentsMap)</code>.
+     */
+    @Beta
+    public void removeEffector(Effector<?> effector) {
+        Effector<?> removed = effectors.remove(effector.getName());
+        invalidateSnapshot();
+        if (removed != null) {
+            instance.emit(AbstractEntity.EFFECTOR_REMOVED, removed.getName());
+        }
+    }
+
+    // --------------------------------------------------
+    
+    /**
+     * Sensors available on this entity.
+     */
+    public Map<String,Sensor<?>> getSensors() {
+        return Collections.unmodifiableMap(sensors);
+    }
+    
+    /** 
+     * Convenience for finding named sensor.
+     */
+    public Sensor<?> getSensor(String sensorName) {
+        return sensors.get(sensorName);
+    }
+
+    /**
+     * Adds the given {@link Sensor} to this entity.
+     */
+    public void addSensor(Sensor<?> newSensor) {
+        sensors.put(newSensor.getName(), newSensor);
+        invalidateSnapshot();
+        instance.emit(AbstractEntity.SENSOR_ADDED, newSensor);
+    }
+    
+    /**
+     * Adds the given {@link Sensor}s to this entity.
+     */
+    public void addSensors(Iterable<? extends Sensor<?>> newSensors) {
+        for (Sensor<?> sensor : newSensors) {
+            addSensor(sensor);
+        }
+    }
+    
+    public void addSensorIfAbsent(Sensor<?> newSensor) {
+        Sensor<?> prev = addSensorIfAbsentWithoutPublishing(newSensor);
+        if (prev == null) {
+            instance.emit(AbstractEntity.SENSOR_ADDED, newSensor);
+        }
+    }
+    
+    public Sensor<?> addSensorIfAbsentWithoutPublishing(Sensor<?> newSensor) {
+        Sensor<?> prev = sensors.putIfAbsent(newSensor.getName(), newSensor);
+        if (prev == null) {
+            invalidateSnapshot();
+        }
+        return prev;
+    }
+
+    /**
+     * Removes the named {@link Sensor} from this entity.
+     */
+    public Sensor<?> removeSensor(String sensorName) {
+        Sensor<?> result = sensors.remove(sensorName);
+        if (result != null) {
+            invalidateSnapshot();
+            instance.emit(AbstractEntity.SENSOR_REMOVED, result);
+        }
+        return result;
+    }
+    
+    /**
+     * Removes the named {@link Sensor} from this entity.
+     */
+    public boolean removeSensor(Sensor<?> sensor) {
+        return (removeSensor(sensor.getName()) != null);
+    }
+    
+    // --------------------------------------------------
+    
+    @Override
+    protected EntityTypeSnapshot newSnapshot() {
+        return new EntityTypeSnapshot(name, value(configKeys), sensors, effectors.values());
+    }
+    
+    /**
+     * Finds the effectors defined on the entity's class, statics and optionally any non-static (discouraged).
+     */
+    public static Map<String,Effector<?>> findEffectors(Class<? extends Entity> clazz, Entity optionalEntity) {
+        try {
+            Map<String,Effector<?>> result = Maps.newLinkedHashMap();
+            Map<String,Field> fieldSources = Maps.newLinkedHashMap();
+            Map<String,Method> methodSources = Maps.newLinkedHashMap();
+            
+            for (Field f : Reflections.findPublicFieldsOrderedBySuper(clazz)) {
+                if (Effector.class.isAssignableFrom(f.getType())) {
+                    if (!Modifier.isStatic(f.getModifiers())) {
+                        // require it to be static or we have an instance
+                        LOG.warn("Discouraged/deprecated use of non-static effector field "+f+" defined in " + (optionalEntity!=null ? optionalEntity : clazz));
+                        if (optionalEntity==null) continue;
+                    }
+                    Effector<?> eff = (Effector<?>) f.get(optionalEntity);
+                    if (eff==null) {
+                        LOG.warn("Effector "+f+" undefined for "+clazz+" ("+optionalEntity+")");
+                        continue;
+                    }
+                    Effector<?> overwritten = result.put(eff.getName(), eff);
+                    Field overwrittenFieldSource = fieldSources.put(eff.getName(), f);
+                    if (overwritten!=null && !Effectors.sameInstance(overwritten, eff)) {
+                        LOG.trace("multiple definitions for effector {} on {}; preferring {} from {} to {} from {}", new Object[] {
+                                eff.getName(), (optionalEntity != null ? optionalEntity : clazz), eff, f, overwritten, 
+                                overwrittenFieldSource});
+                    }
+                }
+            }
+
+            for (Method m : Reflections.findPublicMethodsOrderedBySuper(clazz)) {
+                org.apache.brooklyn.core.annotation.Effector effectorAnnotation = m.getAnnotation(org.apache.brooklyn.core.annotation.Effector.class);
+                if (effectorAnnotation != null) {
+                    if (Modifier.isStatic(m.getModifiers())) {
+                        // require it to be static or we have an instance
+                        LOG.warn("Discouraged/deprecated use of static annotated effector method "+m+" defined in " + (optionalEntity!=null ? optionalEntity : clazz));
+                        if (optionalEntity==null) continue;
+                    }
+
+                    Effector<?> eff = MethodEffector.create(m);
+                    Effector<?> overwritten = result.get(eff.getName());
+                    
+                    if ((overwritten instanceof EffectorWithBody) && !(overwritten instanceof MethodEffector<?>)) {
+                        // don't let annotations on methods override a static, unless that static is a MethodEffector
+                        // TODO not perfect, but approx right; we should clarify whether we prefer statics or methods
+                    } else {
+                        result.put(eff.getName(), eff);
+                        Method overwrittenMethodSource = methodSources.put(eff.getName(), m);
+                        Field overwrittenFieldSource = fieldSources.remove(eff.getName());
+                        LOG.trace("multiple definitions for effector {} on {}; preferring {} from {} to {} from {}", new Object[] {
+                                eff.getName(), (optionalEntity != null ? optionalEntity : clazz), eff, m, overwritten, 
+                                (overwrittenMethodSource != null ? overwrittenMethodSource : overwrittenFieldSource)});
+                    }
+                }
+            }
+
+            return result;
+        } catch (IllegalAccessException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+    
+
+    /**
+     * Finds the sensors defined on the entity's class, statics and optionally any non-static (discouraged).
+     */
+    public static Map<String,Sensor<?>> findSensors(Class<? extends Entity> clazz, Entity optionalEntity) {
+        try {
+            Map<String,Sensor<?>> result = Maps.newLinkedHashMap();
+            Map<String,Field> sources = Maps.newLinkedHashMap();
+            for (Field f : Reflections.findPublicFieldsOrderedBySuper((clazz))) {
+                if (Sensor.class.isAssignableFrom(f.getType())) {
+                    if (!Modifier.isStatic(f.getModifiers())) {
+                        // require it to be static or we have an instance
+                        LOG.warn("Discouraged use of non-static sensor "+f+" defined in " + (optionalEntity!=null ? optionalEntity : clazz));
+                        if (optionalEntity==null) continue;
+                    }
+                    Sensor<?> sens = (Sensor<?>) f.get(optionalEntity);
+                    Sensor<?> overwritten = result.put(sens.getName(), sens);
+                    Field source = sources.put(sens.getName(), f);
+                    if (overwritten!=null && overwritten != sens) {
+                        if (sens instanceof HasConfigKey) {
+                            // probably overriding defaults, just log low level (there will be add'l logging in config key section)
+                            LOG.trace("multiple definitions for config sensor {} on {}; preferring {} from {} to {} from {}", new Object[] {
+                                    sens.getName(), optionalEntity!=null ? optionalEntity : clazz, sens, f, overwritten, source});
+                        } else {
+                            LOG.warn("multiple definitions for sensor {} on {}; preferring {} from {} to {} from {}", new Object[] {
+                                    sens.getName(), optionalEntity!=null ? optionalEntity : clazz, sens, f, overwritten, source});
+                        }
+                    }
+                }
+            }
+
+            return result;
+        } catch (IllegalAccessException e) {
+            throw Throwables.propagate(e);
+        }
+    }
+}