You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/02/18 16:47:36 UTC

[18/34] brooklyn-server git commit: [BROOKLYN-183] REST API using CXF JAX-RS 2.0 implementation

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
new file mode 100644
index 0000000..986855e
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
@@ -0,0 +1,451 @@
+/*
+ * 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.rest.resources;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.status;
+import static javax.ws.rs.core.Response.Status.ACCEPTED;
+import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceAbsoluteUriBuilder;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.objs.BrooklynObject;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.api.sensor.Sensor;
+import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.core.config.ConstraintViolationException;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.EntityPredicates;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.mgmt.EntityManagementUtils;
+import org.apache.brooklyn.core.mgmt.EntityManagementUtils.CreationResult;
+import org.apache.brooklyn.core.mgmt.entitlement.EntitlementPredicates;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.EntityAndItem;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
+import org.apache.brooklyn.core.typereg.RegisteredTypes;
+import org.apache.brooklyn.entity.group.AbstractGroup;
+import org.apache.brooklyn.rest.api.ApplicationApi;
+import org.apache.brooklyn.rest.domain.ApplicationSpec;
+import org.apache.brooklyn.rest.domain.ApplicationSummary;
+import org.apache.brooklyn.rest.domain.EntityDetail;
+import org.apache.brooklyn.rest.domain.EntitySummary;
+import org.apache.brooklyn.rest.domain.TaskSummary;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.ApplicationTransformer;
+import org.apache.brooklyn.rest.transform.EntityTransformer;
+import org.apache.brooklyn.rest.transform.TaskTransformer;
+import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.UserFacingException;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.javalang.JavaClassNames;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+@HaHotStateRequired
+public class ApplicationResource extends AbstractBrooklynRestResource implements ApplicationApi {
+
+    private static final Logger log = LoggerFactory.getLogger(ApplicationResource.class);
+
+    @Context
+    private UriInfo uriInfo;
+
+    private EntityDetail fromEntity(Entity entity) {
+        Boolean serviceUp = entity.getAttribute(Attributes.SERVICE_UP);
+
+        Lifecycle serviceState = entity.getAttribute(Attributes.SERVICE_STATE_ACTUAL);
+
+        String iconUrl = entity.getIconUrl();
+        if (iconUrl!=null) {
+            if (brooklyn().isUrlServerSideAndSafe(iconUrl))
+                // route to server if it is a server-side url
+                iconUrl = EntityTransformer.entityUri(entity, ui.getBaseUriBuilder())+"/icon";
+        }
+
+        List<EntitySummary> children = Lists.newArrayList();
+        if (!entity.getChildren().isEmpty()) {
+            for (Entity child : entity.getChildren()) {
+                children.add(fromEntity(child));
+            }
+        }
+
+        String parentId = null;
+        if (entity.getParent()!= null) {
+            parentId = entity.getParent().getId();
+        }
+
+        List<String> groupIds = Lists.newArrayList();
+        if (!entity.groups().isEmpty()) {
+            groupIds.addAll(entitiesIdAsArray(entity.groups()));
+        }
+
+        List<Map<String, String>> members = Lists.newArrayList();
+        if (entity instanceof Group) {
+            // use attribute instead of method in case it is read-only
+            Collection<Entity> memberEntities = entity.getAttribute(AbstractGroup.GROUP_MEMBERS);
+            if (memberEntities != null && !memberEntities.isEmpty())
+                members.addAll(entitiesIdAndNameAsList(memberEntities));
+        }
+
+        return new EntityDetail(
+                entity.getApplicationId(),
+                entity.getId(),
+                parentId,
+                entity.getDisplayName(),
+                entity.getEntityType().getName(),
+                serviceUp,
+                serviceState,
+                iconUrl,
+                entity.getCatalogItemId(),
+                children,
+                groupIds,
+                members);
+    }
+
+    private List<Map<String, String>> entitiesIdAndNameAsList(Collection<? extends Entity> entities) {
+        List<Map<String, String>> members = Lists.newArrayList();
+        for (Entity entity : entities) {
+            if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                members.add(ImmutableMap.of("id", entity.getId(), "name", entity.getDisplayName()));
+            }
+        }
+        return members;
+    }
+
+    private List<String> entitiesIdAsArray(Iterable<? extends Entity> entities) {
+        List<String> ids = Lists.newArrayList();
+        for (Entity entity : entities) {
+            if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                ids.add(entity.getId());
+            }
+        }
+        return ids;
+    }
+
+    @Override
+    public List<EntityDetail> fetch(String entityIds) {
+
+        List<EntityDetail> entitySummaries = Lists.newArrayList();
+        for (Entity application : mgmt().getApplications()) {
+            entitySummaries.add(fromEntity(application));
+        }
+
+        if (entityIds != null) {
+            for (String entityId: entityIds.split(",")) {
+                Entity entity = mgmt().getEntityManager().getEntity(entityId.trim());
+                while (entity != null && entity.getParent() != null) {
+                    if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                        entitySummaries.add(fromEntity(entity));
+                    }
+                    entity = entity.getParent();
+                }
+            }
+        }
+        return entitySummaries;
+    }
+
+    @Override
+    public List<ApplicationSummary> list(String typeRegex) {
+        if (Strings.isBlank(typeRegex)) {
+            typeRegex = ".*";
+        }
+        return FluentIterable
+                .from(mgmt().getApplications())
+                .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY))
+                .filter(EntityPredicates.hasInterfaceMatching(typeRegex))
+                .transform(ApplicationTransformer.fromApplication(ui.getBaseUriBuilder()))
+                .toList();
+    }
+
+    @Override
+    public ApplicationSummary get(String application) {
+        return ApplicationTransformer.summaryFromApplication(brooklyn().getApplication(application), ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public Response create(ApplicationSpec applicationSpec) {
+        return createFromAppSpec(applicationSpec);
+    }
+
+    /** @deprecated since 0.7.0 see #create */ @Deprecated
+    protected Response createFromAppSpec(ApplicationSpec applicationSpec) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, applicationSpec)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
+                Entitlements.getEntitlementContext().user(), applicationSpec);
+        }
+
+        checkApplicationTypesAreValid(applicationSpec);
+        checkLocationsAreValid(applicationSpec);
+        // TODO duplicate prevention
+        List<Location> locations = brooklyn().getLocations(applicationSpec);
+        Application app = brooklyn().create(applicationSpec);
+        Task<?> t = brooklyn().start(app, locations);
+        TaskSummary ts = TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(t);
+        URI ref = serviceAbsoluteUriBuilder(uriInfo.getBaseUriBuilder(), ApplicationApi.class, "get")
+                .build(app.getApplicationId());
+        return created(ref).entity(ts).build();
+    }
+
+    @Override
+    public Response createFromYaml(String yaml) {
+        // First of all, see if it's a URL
+        URI uri;
+        try {
+            uri = new URI(yaml);
+        } catch (URISyntaxException e) {
+            // It's not a URI then...
+            uri = null;
+        }
+        if (uri != null) {
+            log.debug("Create app called with URI; retrieving contents: {}", uri);
+            yaml = ResourceUtils.create(mgmt()).getResourceAsString(uri.toString());
+        }
+
+        log.debug("Creating app from yaml:\n{}", yaml);
+        EntitySpec<? extends Application> spec = createEntitySpecForApplication(yaml);
+
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
+                Entitlements.getEntitlementContext().user(), yaml);
+        }
+
+        return launch(yaml, spec);
+    }
+
+    private Response launch(String yaml, EntitySpec<? extends Application> spec) {
+        try {
+            Application app = EntityManagementUtils.createUnstarted(mgmt(), spec);
+            CreationResult<Application,Void> result = EntityManagementUtils.start(app);
+
+            boolean isEntitled = Entitlements.isEntitled(
+                    mgmt().getEntitlementManager(),
+                    Entitlements.INVOKE_EFFECTOR,
+                    EntityAndItem.of(app, StringAndArgument.of(Startable.START.getName(), null)));
+
+            if (!isEntitled) {
+                throw WebResourceUtils.forbidden("User '%s' is not authorized to start application %s",
+                    Entitlements.getEntitlementContext().user(), spec.getType());
+            }
+
+            log.info("Launched from YAML: " + yaml + " -> " + app + " (" + result.task() + ")");
+
+            URI ref = serviceAbsoluteUriBuilder(ui.getBaseUriBuilder(), ApplicationApi.class, "get").build(app.getApplicationId());
+            ResponseBuilder response = created(ref);
+            if (result.task() != null)
+                response.entity(TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(result.task()));
+            return response.build();
+        } catch (ConstraintViolationException e) {
+            throw new UserFacingException(e);
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Override
+    public Response createPoly(byte[] inputToAutodetectType) {
+        log.debug("Creating app from autodetecting input");
+
+        boolean looksLikeLegacy = false;
+        Exception legacyFormatException = null;
+        // attempt legacy format
+        try {
+            ApplicationSpec appSpec = mapper().readValue(inputToAutodetectType, ApplicationSpec.class);
+            if (appSpec.getType() != null || appSpec.getEntities() != null) {
+                looksLikeLegacy = true;
+            }
+            return createFromAppSpec(appSpec);
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            legacyFormatException = e;
+            log.debug("Input is not legacy ApplicationSpec JSON (will try others): "+e, e);
+        }
+
+        //TODO infer encoding from request
+        String potentialYaml = new String(inputToAutodetectType);
+        EntitySpec<? extends Application> spec = createEntitySpecForApplication(potentialYaml);
+
+        // TODO not json - try ZIP, etc
+
+        if (spec != null) {
+            return launch(potentialYaml, spec);
+        } else if (looksLikeLegacy) {
+            throw Throwables.propagate(legacyFormatException);
+        } else {
+            return Response.serverError().entity("Unsupported format; not able to autodetect.").build();
+        }
+    }
+
+    @Override
+    public Response createFromForm(String contents) {
+        log.debug("Creating app from form");
+        return createPoly(contents.getBytes());
+    }
+
+    @Override
+    public Response delete(String application) {
+        Application app = brooklyn().getApplication(application);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(app,
+            StringAndArgument.of(Entitlements.LifecycleEffectors.DELETE, null)))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to delete application %s",
+                Entitlements.getEntitlementContext().user(), app);
+        }
+        Task<?> t = brooklyn().destroy(app);
+        TaskSummary ts = TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(t);
+        return status(ACCEPTED).entity(ts).build();
+    }
+
+    private EntitySpec<? extends Application> createEntitySpecForApplication(String potentialYaml) {
+        try {
+            return EntityManagementUtils.createEntitySpecForApplication(mgmt(), potentialYaml);
+        } catch (Exception e) {
+            // An IllegalArgumentException for creating the entity spec gets wrapped in a ISE, and possibly a Compound.
+            // But we want to return a 400 rather than 500, so ensure we throw IAE.
+            IllegalArgumentException iae = (IllegalArgumentException) Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class);
+            if (iae != null) {
+                throw new IllegalArgumentException("Cannot create spec for app: "+iae.getMessage(), e);
+            } else {
+                throw Exceptions.propagate(e);
+            }
+        }
+    }
+
+    private void checkApplicationTypesAreValid(ApplicationSpec applicationSpec) {
+        String appType = applicationSpec.getType();
+        if (appType != null) {
+            checkEntityTypeIsValid(appType);
+
+            if (applicationSpec.getEntities() != null) {
+                throw WebResourceUtils.preconditionFailed("Application given explicit type '%s' must not define entities", appType);
+            }
+            return;
+        }
+
+        for (org.apache.brooklyn.rest.domain.EntitySpec entitySpec : applicationSpec.getEntities()) {
+            String entityType = entitySpec.getType();
+            checkEntityTypeIsValid(checkNotNull(entityType, "entityType"));
+        }
+    }
+
+    private void checkSpecTypeIsValid(String type, Class<? extends BrooklynObject> subType) {
+        Maybe<RegisteredType> typeV = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType));
+        if (!typeV.isNull()) {
+            // found, throw if any problem
+            typeV.get();
+            return;
+        }
+
+        // not found, try classloading
+        try {
+            brooklyn().getCatalogClassLoader().loadClass(type);
+        } catch (ClassNotFoundException e) {
+            log.debug("Class not found for type '" + type + "'; reporting 404", e);
+            throw WebResourceUtils.notFound("Undefined type '%s'", type);
+        }
+        log.info(JavaClassNames.simpleClassName(subType)+" type '{}' not defined in catalog but is on classpath; continuing", type);
+    }
+
+    private void checkEntityTypeIsValid(String type) {
+        checkSpecTypeIsValid(type, Entity.class);
+    }
+
+    @SuppressWarnings("deprecation")
+    private void checkLocationsAreValid(ApplicationSpec applicationSpec) {
+        for (String locationId : applicationSpec.getLocations()) {
+            locationId = BrooklynRestResourceUtils.fixLocation(locationId);
+            if (!brooklyn().getLocationRegistry().canMaybeResolve(locationId) && brooklyn().getLocationRegistry().getDefinedLocationById(locationId)==null) {
+                throw WebResourceUtils.notFound("Undefined location '%s'", locationId);
+            }
+        }
+    }
+
+    @Override
+    public List<EntitySummary> getDescendants(String application, String typeRegex) {
+        return EntityTransformer.entitySummaries(brooklyn().descendantsOfType(application, application, typeRegex), ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public Map<String, Object> getDescendantsSensor(String application, String sensor, String typeRegex) {
+        Iterable<Entity> descs = brooklyn().descendantsOfType(application, application, typeRegex);
+        return getSensorMap(sensor, descs);
+    }
+
+    public static Map<String, Object> getSensorMap(String sensor, Iterable<Entity> descs) {
+        if (Iterables.isEmpty(descs))
+            return Collections.emptyMap();
+        Map<String, Object> result = MutableMap.of();
+        Iterator<Entity> di = descs.iterator();
+        Sensor<?> s = null;
+        while (di.hasNext()) {
+            Entity potentialSource = di.next();
+            s = potentialSource.getEntityType().getSensor(sensor);
+            if (s!=null) break;
+        }
+        if (s==null)
+            s = Sensors.newSensor(Object.class, sensor);
+        if (!(s instanceof AttributeSensor<?>)) {
+            log.warn("Cannot retrieve non-attribute sensor "+s+" for entities; returning empty map");
+            return result;
+        }
+        for (Entity e: descs) {
+            Object v = null;
+            try {
+                v = e.getAttribute((AttributeSensor<?>)s);
+            } catch (Exception exc) {
+                Exceptions.propagateIfFatal(exc);
+                log.warn("Error retrieving sensor "+s+" for "+e+" (ignoring): "+exc);
+            }
+            if (v!=null)
+                result.put(e.getId(), v);
+        }
+        return result;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
new file mode 100644
index 0000000..d7b206a
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
@@ -0,0 +1,509 @@
+/*
+ * 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.rest.resources;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.apache.brooklyn.api.catalog.CatalogItem;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.api.policy.PolicySpec;
+import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.core.catalog.CatalogPredicates;
+import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
+import org.apache.brooklyn.core.catalog.internal.CatalogDto;
+import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator;
+import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
+import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
+import org.apache.brooklyn.core.typereg.RegisteredTypes;
+import org.apache.brooklyn.rest.api.CatalogApi;
+import org.apache.brooklyn.rest.domain.ApiError;
+import org.apache.brooklyn.rest.domain.CatalogEntitySummary;
+import org.apache.brooklyn.rest.domain.CatalogItemSummary;
+import org.apache.brooklyn.rest.domain.CatalogLocationSummary;
+import org.apache.brooklyn.rest.domain.CatalogPolicySummary;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.CatalogTransformer;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.StringPredicates;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import javax.ws.rs.core.UriInfo;
+import org.apache.brooklyn.util.guava.Maybe;
+
+@HaHotStateRequired
+public class CatalogResource extends AbstractBrooklynRestResource implements CatalogApi {
+
+    private static final Logger log = LoggerFactory.getLogger(CatalogResource.class);
+    
+    @SuppressWarnings("rawtypes")
+    private Function<CatalogItem, CatalogItemSummary> toCatalogItemSummary(final UriInfo ui) {
+        return new Function<CatalogItem, CatalogItemSummary>() {
+            @Override
+            public CatalogItemSummary apply(@Nullable CatalogItem input) {
+                return CatalogTransformer.catalogItemSummary(brooklyn(), input, ui.getBaseUriBuilder());
+            }
+        };
+    };
+
+    static Set<String> missingIcons = MutableSet.of();
+    
+    @Override
+    public Response create(String yaml) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, yaml)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to add catalog item",
+                Entitlements.getEntitlementContext().user());
+        }
+        
+        Iterable<? extends CatalogItem<?, ?>> items; 
+        try {
+            items = brooklyn().getCatalog().addItems(yaml);
+        } catch (IllegalArgumentException e) {
+            return Response.status(Status.BAD_REQUEST)
+                    .type(MediaType.APPLICATION_JSON)
+                    .entity(ApiError.of(e))
+                    .build();
+        }
+
+        log.info("REST created catalog items: "+items);
+
+        Map<String,Object> result = MutableMap.of();
+        
+        for (CatalogItem<?,?> item: items) {
+            result.put(item.getId(), CatalogTransformer.catalogItemSummary(brooklyn(), item, ui.getBaseUriBuilder()));
+        }
+        return Response.status(Status.CREATED).entity(result).build();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public Response resetXml(String xml, boolean ignoreErrors) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, null) ||
+            !Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, null)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        ((BasicBrooklynCatalog)mgmt().getCatalog()).reset(CatalogDto.newDtoFromXmlContents(xml, "REST reset"), !ignoreErrors);
+        return Response.ok().build();
+    }
+    
+    @Override
+    @Deprecated
+    public void deleteEntity_0_7_0(String entityId) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(entityId, "delete"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                Entitlements.getEntitlementContext().user());
+        }
+        try {
+            Maybe<RegisteredType> item = RegisteredTypes.tryValidate(mgmt().getTypeRegistry().get(entityId), RegisteredTypeLoadingContexts.spec(Entity.class));
+            if (item.isNull()) {
+                throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId);
+            }
+            if (item.isAbsent()) {
+                throw WebResourceUtils.notFound("Item with id '%s' is not an entity", entityId);
+            }
+
+            brooklyn().getCatalog().deleteCatalogItem(item.get().getSymbolicName(), item.get().getVersion());
+
+        } catch (NoSuchElementException e) {
+            // shouldn't come here
+            throw WebResourceUtils.notFound("Entity with id '%s' could not be deleted", entityId);
+
+        }
+    }
+
+    @Override
+    public void deleteApplication(String symbolicName, String version) throws Exception {
+        deleteEntity(symbolicName, version);
+    }
+
+    @Override
+    public void deleteEntity(String symbolicName, String version) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(symbolicName+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                Entitlements.getEntitlementContext().user());
+        }
+        
+        RegisteredType item = mgmt().getTypeRegistry().get(symbolicName, version);
+        if (item == null) {
+            throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version);
+        } else if (!RegisteredTypePredicates.IS_ENTITY.apply(item) && !RegisteredTypePredicates.IS_APPLICATION.apply(item)) {
+            throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not an entity", symbolicName, version);
+        } else {
+            brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion());
+        }
+    }
+
+    @Override
+    public void deletePolicy(String policyId, String version) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(policyId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                Entitlements.getEntitlementContext().user());
+        }
+        
+        RegisteredType item = mgmt().getTypeRegistry().get(policyId, version);
+        if (item == null) {
+            throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version);
+        } else if (!RegisteredTypePredicates.IS_POLICY.apply(item)) {
+            throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a policy", policyId, version);
+        } else {
+            brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion());
+        }
+    }
+
+    @Override
+    public void deleteLocation(String locationId, String version) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(locationId+(Strings.isBlank(version) ? "" : ":"+version), "delete"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                Entitlements.getEntitlementContext().user());
+        }
+        
+        RegisteredType item = mgmt().getTypeRegistry().get(locationId, version);
+        if (item == null) {
+            throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version);
+        } else if (!RegisteredTypePredicates.IS_LOCATION.apply(item)) {
+            throw WebResourceUtils.preconditionFailed("Item with id '%s:%s' not a location", locationId, version);
+        } else {
+            brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion());
+        }
+    }
+
+    @Override
+    public List<CatalogEntitySummary> listEntities(String regex, String fragment, boolean allVersions) {
+        Predicate<CatalogItem<Entity, EntitySpec<?>>> filter =
+                Predicates.and(
+                        CatalogPredicates.IS_ENTITY,
+                        CatalogPredicates.<Entity, EntitySpec<?>>disabled(false));
+        List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions);
+        return castList(result, CatalogEntitySummary.class);
+    }
+
+    @Override
+    public List<CatalogItemSummary> listApplications(String regex, String fragment, boolean allVersions) {
+        @SuppressWarnings("unchecked")
+        Predicate<CatalogItem<Application, EntitySpec<? extends Application>>> filter =
+                Predicates.and(
+                        CatalogPredicates.IS_TEMPLATE,
+                        CatalogPredicates.<Application,EntitySpec<? extends Application>>deprecated(false),
+                        CatalogPredicates.<Application,EntitySpec<? extends Application>>disabled(false));
+        return getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions);
+    }
+
+    @Override
+    @Deprecated
+    public CatalogEntitySummary getEntity_0_7_0(String entityId) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, entityId)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        CatalogItem<Entity,EntitySpec<?>> result =
+                CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Entity.class, entityId);
+
+        if (result==null) {
+            throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId);
+        }
+
+        return CatalogTransformer.catalogEntitySummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+    
+    @Override
+    public CatalogEntitySummary getEntity(String symbolicName, String version) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, symbolicName+(Strings.isBlank(version)?"":":"+version))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        //TODO These casts are not pretty, we could just provide separate get methods for the different types?
+        //Or we could provide asEntity/asPolicy cast methods on the CataloItem doing a safety check internally
+        @SuppressWarnings("unchecked")
+        CatalogItem<Entity, EntitySpec<?>> result =
+              (CatalogItem<Entity, EntitySpec<?>>) brooklyn().getCatalog().getCatalogItem(symbolicName, version);
+
+        if (result==null) {
+            throw WebResourceUtils.notFound("Entity with id '%s:%s' not found", symbolicName, version);
+        }
+
+        return CatalogTransformer.catalogEntitySummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+
+    @Override
+    @Deprecated
+    public CatalogEntitySummary getApplication_0_7_0(String applicationId) throws Exception {
+        return getEntity_0_7_0(applicationId);
+    }
+
+    @Override
+    public CatalogEntitySummary getApplication(String symbolicName, String version) {
+        return getEntity(symbolicName, version);
+    }
+
+    @Override
+    public List<CatalogPolicySummary> listPolicies(String regex, String fragment, boolean allVersions) {
+        Predicate<CatalogItem<Policy, PolicySpec<?>>> filter =
+                Predicates.and(
+                        CatalogPredicates.IS_POLICY,
+                        CatalogPredicates.<Policy, PolicySpec<?>>disabled(false));
+        List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions);
+        return castList(result, CatalogPolicySummary.class);
+    }
+
+    @Override
+    @Deprecated
+    public CatalogPolicySummary getPolicy_0_7_0(String policyId) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        CatalogItem<? extends Policy, PolicySpec<?>> result =
+            CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Policy.class, policyId);
+
+        if (result==null) {
+            throw WebResourceUtils.notFound("Policy with id '%s' not found", policyId);
+        }
+
+        return CatalogTransformer.catalogPolicySummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public CatalogPolicySummary getPolicy(String policyId, String version) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId+(Strings.isBlank(version)?"":":"+version))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        @SuppressWarnings("unchecked")
+        CatalogItem<? extends Policy, PolicySpec<?>> result =
+                (CatalogItem<? extends Policy, PolicySpec<?>>)brooklyn().getCatalog().getCatalogItem(policyId, version);
+
+        if (result==null) {
+          throw WebResourceUtils.notFound("Policy with id '%s:%s' not found", policyId, version);
+        }
+
+        return CatalogTransformer.catalogPolicySummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public List<CatalogLocationSummary> listLocations(String regex, String fragment, boolean allVersions) {
+        Predicate<CatalogItem<Location, LocationSpec<?>>> filter =
+                Predicates.and(
+                        CatalogPredicates.IS_LOCATION,
+                        CatalogPredicates.<Location, LocationSpec<?>>disabled(false));
+        List<CatalogItemSummary> result = getCatalogItemSummariesMatchingRegexFragment(filter, regex, fragment, allVersions);
+        return castList(result, CatalogLocationSummary.class);
+    }
+
+    @Override
+    @Deprecated
+    public CatalogLocationSummary getLocation_0_7_0(String locationId) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        CatalogItem<? extends Location, LocationSpec<?>> result =
+            CatalogUtils.getCatalogItemOptionalVersion(mgmt(), Location.class, locationId);
+
+        if (result==null) {
+            throw WebResourceUtils.notFound("Location with id '%s' not found", locationId);
+        }
+
+        return CatalogTransformer.catalogLocationSummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public CatalogLocationSummary getLocation(String locationId, String version) throws Exception {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId+(Strings.isBlank(version)?"":":"+version))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        @SuppressWarnings("unchecked")
+        CatalogItem<? extends Location, LocationSpec<?>> result =
+                (CatalogItem<? extends Location, LocationSpec<?>>)brooklyn().getCatalog().getCatalogItem(locationId, version);
+
+        if (result==null) {
+          throw WebResourceUtils.notFound("Location with id '%s:%s' not found", locationId, version);
+        }
+
+        return CatalogTransformer.catalogLocationSummary(brooklyn(), result, ui.getBaseUriBuilder());
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private <T,SpecT> List<CatalogItemSummary> getCatalogItemSummariesMatchingRegexFragment(Predicate<CatalogItem<T,SpecT>> type, String regex, String fragment, boolean allVersions) {
+        List filters = new ArrayList();
+        filters.add(type);
+        if (Strings.isNonEmpty(regex))
+            filters.add(CatalogPredicates.xml(StringPredicates.containsRegex(regex)));
+        if (Strings.isNonEmpty(fragment))
+            filters.add(CatalogPredicates.xml(StringPredicates.containsLiteralIgnoreCase(fragment)));
+        if (!allVersions)
+            filters.add(CatalogPredicates.isBestVersion(mgmt()));
+        
+        filters.add(CatalogPredicates.entitledToSee(mgmt()));
+
+        ImmutableList<CatalogItem<Object, Object>> sortedItems =
+                FluentIterable.from(brooklyn().getCatalog().getCatalogItems())
+                    .filter(Predicates.and(filters))
+                    .toSortedList(CatalogItemComparator.getInstance());
+        return Lists.transform(sortedItems, toCatalogItemSummary(ui));
+    }
+
+    @Override
+    @Deprecated
+    public Response getIcon_0_7_0(String itemId) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        return getCatalogItemIcon( mgmt().getTypeRegistry().get(itemId) );
+    }
+
+    @Override
+    public Response getIcon(String itemId, String version) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId+(Strings.isBlank(version)?"":":"+version))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see catalog entry",
+                Entitlements.getEntitlementContext().user());
+        }
+        
+        return getCatalogItemIcon(mgmt().getTypeRegistry().get(itemId, version));
+    }
+
+    @Override
+    public void setDeprecatedLegacy(String itemId, boolean deprecated) {
+        log.warn("Use of deprecated \"/catalog/entities/{itemId}/deprecated/{deprecated}\" for "+itemId
+                +"; use \"/catalog/entities/{itemId}/deprecated\" with request body");
+        setDeprecated(itemId, deprecated);
+    }
+    
+    @SuppressWarnings("deprecation")
+    @Override
+    public void setDeprecated(String itemId, boolean deprecated) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "deprecated"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                    Entitlements.getEntitlementContext().user());
+        }
+        CatalogUtils.setDeprecated(mgmt(), itemId, deprecated);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void setDisabled(String itemId, boolean disabled) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(itemId, "disabled"))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify catalog",
+                    Entitlements.getEntitlementContext().user());
+        }
+        CatalogUtils.setDisabled(mgmt(), itemId, disabled);
+    }
+
+    private Response getCatalogItemIcon(RegisteredType result) {
+        String url = result.getIconUrl();
+        if (url==null) {
+            log.debug("No icon available for "+result+"; returning "+Status.NO_CONTENT);
+            return Response.status(Status.NO_CONTENT).build();
+        }
+        
+        if (brooklyn().isUrlServerSideAndSafe(url)) {
+            // classpath URL's we will serve IF they end with a recognised image format;
+            // paths (ie non-protocol) and 
+            // NB, for security, file URL's are NOT served
+            log.debug("Loading and returning "+url+" as icon for "+result);
+            
+            MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(Files.getFileExtension(url));
+            try {
+                Object content = ResourceUtils.create(CatalogUtils.newClassLoadingContext(mgmt(), result)).getResourceFromUrl(url);
+                return Response.ok(content, mime).build();
+            } catch (Exception e) {
+                Exceptions.propagateIfFatal(e);
+                synchronized (missingIcons) {
+                    if (missingIcons.add(url)) {
+                        // note: this can be quite common when running from an IDE, as resources may not be copied;
+                        // a mvn build should sort it out (the IDE will then find the resources, until you clean or maybe refresh...)
+                        log.warn("Missing icon data for "+result.getId()+", expected at: "+url+" (subsequent messages will log debug only)");
+                        log.debug("Trace for missing icon data at "+url+": "+e, e);
+                    } else {
+                        log.debug("Missing icon data for "+result.getId()+", expected at: "+url+" (already logged WARN and error details)");
+                    }
+                }
+                throw WebResourceUtils.notFound("Icon unavailable for %s", result.getId());
+            }
+        }
+        
+        log.debug("Returning redirect to "+url+" as icon for "+result);
+        
+        // for anything else we do a redirect (e.g. http / https; perhaps ftp)
+        return Response.temporaryRedirect(URI.create(url)).build();
+    }
+
+    // TODO Move to an appropriate utility class?
+    @SuppressWarnings("unchecked")
+    private static <T> List<T> castList(List<? super T> list, Class<T> elementType) {
+        List<T> result = Lists.newArrayList();
+        Iterator<? super T> li = list.iterator();
+        while (li.hasNext()) {
+            try {
+                result.add((T) li.next());
+            } catch (Throwable throwable) {
+                if (throwable instanceof NoClassDefFoundError) {
+                    // happens if class cannot be loaded for any reason during transformation - don't treat as fatal
+                } else {
+                    Exceptions.propagateIfFatal(throwable);
+                }
+                
+                // item cannot be transformed; we will have logged a warning earlier
+                log.debug("Ignoring invalid catalog item: "+throwable);
+            }
+        }
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EffectorResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EffectorResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EffectorResource.java
new file mode 100644
index 0000000..22661bd
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EffectorResource.java
@@ -0,0 +1,114 @@
+/*
+ * 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.rest.resources;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.core.Response;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.mgmt.internal.EffectorUtils;
+import org.apache.brooklyn.rest.api.EffectorApi;
+import org.apache.brooklyn.rest.domain.EffectorSummary;
+import org.apache.brooklyn.rest.domain.SummaryComparators;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.EffectorTransformer;
+import org.apache.brooklyn.rest.transform.TaskTransformer;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+
+@HaHotStateRequired
+public class EffectorResource extends AbstractBrooklynRestResource implements EffectorApi {
+
+    private static final Logger log = LoggerFactory.getLogger(EffectorResource.class);
+
+    @Override
+    public List<EffectorSummary> list(final String application, final String entityToken) {
+        final Entity entity = brooklyn().getEntity(application, entityToken);
+        return FluentIterable
+                .from(entity.getEntityType().getEffectors())
+                .filter(new Predicate<Effector<?>>() {
+                    @Override
+                    public boolean apply(@Nullable Effector<?> input) {
+                        return Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR,
+                                Entitlements.EntityAndItem.of(entity, StringAndArgument.of(input.getName(), null)));
+                    }
+                })
+                .transform(new Function<Effector<?>, EffectorSummary>() {
+                    @Override
+                    public EffectorSummary apply(Effector<?> effector) {
+                        return EffectorTransformer.effectorSummary(entity, effector, ui.getBaseUriBuilder());
+                    }
+                })
+                .toSortedList(SummaryComparators.nameComparator());
+    }
+
+    @Override
+    public Response invoke(String application, String entityToken, String effectorName,
+            String timeout, Map<String, Object> parameters) {
+        final Entity entity = brooklyn().getEntity(application, entityToken);
+
+        // TODO check effectors?
+        Maybe<Effector<?>> effector = EffectorUtils.findEffectorDeclared(entity, effectorName);
+        if (effector.isAbsentOrNull()) {
+            throw WebResourceUtils.notFound("Entity '%s' has no effector with name '%s'", entityToken, effectorName);
+        } else if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.INVOKE_EFFECTOR,
+                Entitlements.EntityAndItem.of(entity, StringAndArgument.of(effector.get().getName(), null)))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to invoke effector %s on entity %s",
+                    Entitlements.getEntitlementContext().user(), effector.get().getName(), entity);
+        }
+        log.info("REST invocation of " + entity + "." + effector.get() + " " + parameters);
+        Task<?> t = entity.invoke(effector.get(), parameters);
+
+        try {
+            Object result;
+            if (timeout == null || timeout.isEmpty() || "never".equalsIgnoreCase(timeout)) {
+                result = t.get();
+            } else {
+                long timeoutMillis = "always".equalsIgnoreCase(timeout) ? 0 : Time.parseElapsedTime(timeout);
+                try {
+                    if (timeoutMillis == 0) throw new TimeoutException();
+                    result = t.get(timeoutMillis, TimeUnit.MILLISECONDS);
+                } catch (TimeoutException e) {
+                    result = TaskTransformer.taskSummary(t, ui.getBaseUriBuilder());
+                }
+            }
+            return Response.status(Response.Status.ACCEPTED).entity(result).build();
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityConfigResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityConfigResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityConfigResource.java
new file mode 100644
index 0000000..4c3c9d2
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityConfigResource.java
@@ -0,0 +1,206 @@
+/*
+ * 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.rest.resources;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.EntityAndItem;
+import org.apache.brooklyn.rest.api.EntityConfigApi;
+import org.apache.brooklyn.rest.domain.EntityConfigSummary;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.EntityTransformer;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.ValueResolver;
+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.Predicates;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+@HaHotStateRequired
+public class EntityConfigResource extends AbstractBrooklynRestResource implements EntityConfigApi {
+
+    private static final Logger LOG = LoggerFactory.getLogger(EntityConfigResource.class);
+
+    @Override
+    public List<EntityConfigSummary> list(final String application, final String entityToken) {
+        final Entity entity = brooklyn().getEntity(application, entityToken);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entity);
+        }
+
+        // TODO merge with keys which have values:
+        //      ((EntityInternal) entity).config().getBag().getAllConfigAsConfigKeyMap();
+        List<EntityConfigSummary> result = Lists.newArrayList();
+        
+        for (ConfigKey<?> key : entity.getEntityType().getConfigKeys()) {
+            // Exclude config that user is not allowed to see
+            if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CONFIG, new EntityAndItem<String>(entity, key.getName()))) {
+                LOG.trace("User {} not authorized to see config {} of entity {}; excluding from ConfigKey list results", 
+                        new Object[] {Entitlements.getEntitlementContext().user(), key.getName(), entity});
+                continue;
+            }
+            result.add(EntityTransformer.entityConfigSummary(entity, key, ui.getBaseUriBuilder()));
+        }
+        
+        return result;
+    }
+
+    // TODO support parameters  ?show=value,summary&name=xxx &format={string,json,xml}
+    // (and in sensors class)
+    @Override
+    public Map<String, Object> batchConfigRead(String application, String entityToken, Boolean raw) {
+        // TODO: add test
+        Entity entity = brooklyn().getEntity(application, entityToken);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entity);
+        }
+
+        // wrap in a task for better runtime view
+        return Entities.submit(entity, Tasks.<Map<String,Object>>builder().displayName("REST API batch config read").body(new BatchConfigRead(mgmt(), this, entity, raw)).build()).getUnchecked();
+    }
+    
+    private static class BatchConfigRead implements Callable<Map<String,Object>> {
+        private final ManagementContext mgmt;
+        private final EntityConfigResource resource;
+        private final Entity entity;
+        private final Boolean raw;
+
+        public BatchConfigRead(ManagementContext mgmt, EntityConfigResource resource, Entity entity, Boolean raw) {
+            this.mgmt = mgmt;
+            this.resource = resource;
+            this.entity = entity;
+            this.raw = raw;
+        }
+
+        @Override
+        public Map<String, Object> call() throws Exception {
+            Map<ConfigKey<?>, ?> source = ((EntityInternal) entity).config().getBag().getAllConfigAsConfigKeyMap();
+            Map<String, Object> result = Maps.newLinkedHashMap();
+            for (Map.Entry<ConfigKey<?>, ?> ek : source.entrySet()) {
+                ConfigKey<?> key = ek.getKey();
+                Object value = ek.getValue();
+                
+                // Exclude config that user is not allowed to see
+                if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_CONFIG, new EntityAndItem<String>(entity, ek.getKey().getName()))) {
+                    LOG.trace("User {} not authorized to see sensor {} of entity {}; excluding from current-state results", 
+                            new Object[] {Entitlements.getEntitlementContext().user(), ek.getKey().getName(), entity});
+                    continue;
+                }
+                result.put(key.getName(), 
+                    resource.resolving(value).preferJson(true).asJerseyOutermostReturnValue(false).raw(raw).context(entity).timeout(Duration.ZERO).renderAs(key).resolve());
+            }
+            return result;
+        }
+    }
+
+    @Override
+    public Object get(String application, String entityToken, String configKeyName, Boolean raw) {
+        return get(true, application, entityToken, configKeyName, raw);
+    }
+
+    @Override
+    public String getPlain(String application, String entityToken, String configKeyName, Boolean raw) {
+        return Strings.toString(get(false, application, entityToken, configKeyName, raw));
+    }
+
+    public Object get(boolean preferJson, String application, String entityToken, String configKeyName, Boolean raw) {
+        Entity entity = brooklyn().getEntity(application, entityToken);
+        ConfigKey<?> ck = findConfig(entity, configKeyName);
+        
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entity);
+        }
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CONFIG, new EntityAndItem<String>(entity, ck.getName()))) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to see entity '%s' config '%s'",
+                    Entitlements.getEntitlementContext().user(), entity, ck.getName());
+        }
+        
+        Object value = ((EntityInternal)entity).config().getRaw(ck).orNull();
+        return resolving(value).preferJson(preferJson).asJerseyOutermostReturnValue(true).raw(raw).context(entity).timeout(ValueResolver.PRETTY_QUICK_WAIT).renderAs(ck).resolve();
+    }
+
+    private ConfigKey<?> findConfig(Entity entity, String configKeyName) {
+        ConfigKey<?> ck = entity.getEntityType().getConfigKey(configKeyName);
+        if (ck == null)
+            ck = new BasicConfigKey<Object>(Object.class, configKeyName);
+        return ck;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Override
+    public void setFromMap(String application, String entityToken, Boolean recurse, Map newValues) {
+        final Entity entity = brooklyn().getEntity(application, entityToken);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_ENTITY, entity)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entity);
+        }
+
+        if (LOG.isDebugEnabled())
+            LOG.debug("REST user " + Entitlements.getEntitlementContext() + " setting configs " + newValues);
+        for (Object entry : newValues.entrySet()) {
+            String configName = Strings.toString(((Map.Entry) entry).getKey());
+            Object newValue = ((Map.Entry) entry).getValue();
+
+            ConfigKey ck = findConfig(entity, configName);
+            ((EntityInternal) entity).config().set(ck, TypeCoercions.coerce(newValue, ck.getTypeToken()));
+            if (Boolean.TRUE.equals(recurse)) {
+                for (Entity e2 : Entities.descendants(entity, Predicates.alwaysTrue(), false)) {
+                    ((EntityInternal) e2).config().set(ck, newValue);
+                }
+            }
+        }
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Override
+    public void set(String application, String entityToken, String configName, Boolean recurse, Object newValue) {
+        final Entity entity = brooklyn().getEntity(application, entityToken);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_ENTITY, entity)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entity);
+        }
+
+        ConfigKey ck = findConfig(entity, configName);
+        LOG.debug("REST setting config " + configName + " on " + entity + " to " + newValue);
+        ((EntityInternal) entity).config().set(ck, TypeCoercions.coerce(newValue, ck.getTypeToken()));
+        if (Boolean.TRUE.equals(recurse)) {
+            for (Entity e2 : Entities.descendants(entity, Predicates.alwaysTrue(), false)) {
+                ((EntityInternal) e2).config().set(ck, newValue);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityResource.java
new file mode 100644
index 0000000..4d263da
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/EntityResource.java
@@ -0,0 +1,223 @@
+/*
+ * 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.rest.resources;
+
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.status;
+import static javax.ws.rs.core.Response.Status.ACCEPTED;
+
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.core.mgmt.BrooklynTags;
+import org.apache.brooklyn.core.mgmt.BrooklynTags.NamedStringTag;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.mgmt.EntityManagementUtils;
+import org.apache.brooklyn.core.mgmt.EntityManagementUtils.CreationResult;
+import org.apache.brooklyn.core.mgmt.entitlement.EntitlementPredicates;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.rest.api.EntityApi;
+import org.apache.brooklyn.rest.domain.EntitySummary;
+import org.apache.brooklyn.rest.domain.LocationSummary;
+import org.apache.brooklyn.rest.domain.TaskSummary;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.EntityTransformer;
+import org.apache.brooklyn.rest.transform.LocationTransformer;
+import org.apache.brooklyn.rest.transform.LocationTransformer.LocationDetailLevel;
+import org.apache.brooklyn.rest.transform.TaskTransformer;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.collections.MutableList;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceAbsoluteUriBuilder;
+
+@HaHotStateRequired
+public class EntityResource extends AbstractBrooklynRestResource implements EntityApi {
+
+    private static final Logger log = LoggerFactory.getLogger(EntityResource.class);
+
+    @Context
+    private UriInfo uriInfo;
+    
+    @Override
+    public List<EntitySummary> list(final String application) {
+        return FluentIterable
+                .from(brooklyn().getApplication(application).getChildren())
+                .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY))
+                .transform(EntityTransformer.fromEntity(ui.getBaseUriBuilder()))
+                .toList();
+    }
+
+    @Override
+    public EntitySummary get(String application, String entityName) {
+        Entity entity = brooklyn().getEntity(application, entityName);
+        if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+            return EntityTransformer.entitySummary(entity, ui.getBaseUriBuilder());
+        }
+        throw WebResourceUtils.forbidden("User '%s' is not authorized to get entity '%s'",
+                Entitlements.getEntitlementContext().user(), entity);
+    }
+
+    @Override
+    public List<EntitySummary> getChildren(final String application, final String entity) {
+        return FluentIterable
+                .from(brooklyn().getEntity(application, entity).getChildren())
+                .filter(EntitlementPredicates.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY))
+                .transform(EntityTransformer.fromEntity(ui.getBaseUriBuilder()))
+                .toList();
+    }
+
+    @Override
+    public List<EntitySummary> getChildrenOld(String application, String entity) {
+        log.warn("Using deprecated call to /entities when /children should be used");
+        return getChildren(application, entity);
+    }
+
+    @Override
+    public Response addChildren(String applicationToken, String entityToken, Boolean start, String timeoutS, String yaml) {
+        final Entity parent = brooklyn().getEntity(applicationToken, entityToken);
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_ENTITY, parent)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to modify entity '%s'",
+                    Entitlements.getEntitlementContext().user(), entityToken);
+        }
+        CreationResult<List<Entity>, List<String>> added = EntityManagementUtils.addChildren(parent, yaml, start)
+                .blockUntilComplete(timeoutS==null ? Duration.millis(20) : Duration.of(timeoutS));
+        ResponseBuilder response;
+        
+        if (added.get().size()==1) {
+            Entity child = Iterables.getOnlyElement(added.get());
+            URI ref = serviceAbsoluteUriBuilder(uriInfo.getBaseUriBuilder(), EntityApi.class, "get")
+                    .build(child.getApplicationId(), child.getId());
+            response = created(ref);
+        } else {
+            response = Response.status(Status.CREATED);
+        }
+        return response.entity(TaskTransformer.taskSummary(added.task(), ui.getBaseUriBuilder())).build();
+    }
+
+    @Override
+    public List<TaskSummary> listTasks(String applicationId, String entityId) {
+        Entity entity = brooklyn().getEntity(applicationId, entityId);
+        Set<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(mgmt().getExecutionManager(), entity);
+        return new LinkedList<TaskSummary>(Collections2.transform(tasks, 
+                TaskTransformer.fromTask(ui.getBaseUriBuilder())));
+    }
+
+    @Override
+    public TaskSummary getTask(final String application, final String entityToken, String taskId) {
+        // TODO deprecate in favour of ActivityApi.get ?
+        Task<?> t = mgmt().getExecutionManager().getTask(taskId);
+        if (t == null)
+            throw WebResourceUtils.notFound("Cannot find task '%s'", taskId);
+        return TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(t);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public List<Object> listTags(String applicationId, String entityId) {
+        Entity entity = brooklyn().getEntity(applicationId, entityId);
+        return (List<Object>) resolving(MutableList.copyOf(entity.tags().getTags())).preferJson(true).resolve();
+    }
+
+    @Override
+    public Response getIcon(String applicationId, String entityId) {
+        Entity entity = brooklyn().getEntity(applicationId, entityId);
+        String url = entity.getIconUrl();
+        if (url == null)
+            return Response.status(Status.NO_CONTENT).build();
+
+        if (brooklyn().isUrlServerSideAndSafe(url)) {
+            // classpath URL's we will serve IF they end with a recognised image format;
+            // paths (ie non-protocol) and
+            // NB, for security, file URL's are NOT served
+            MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(Files.getFileExtension(url));
+            Object content = ResourceUtils.create(brooklyn().getCatalogClassLoader()).getResourceFromUrl(url);
+            return Response.ok(content, mime).build();
+        }
+
+        // for anything else we do a redirect (e.g. http / https; perhaps ftp)
+        return Response.temporaryRedirect(URI.create(url)).build();
+    }
+
+    @Override
+    public Response rename(String application, String entity, String newName) {
+        Entity entityLocal = brooklyn().getEntity(application, entity);
+        entityLocal.setDisplayName(newName);
+        return status(Response.Status.OK).build();
+    }
+
+    @Override
+    public Response expunge(String application, String entity, boolean release) {
+        Entity entityLocal = brooklyn().getEntity(application, entity);
+        Task<?> task = brooklyn().expunge(entityLocal, release);
+        TaskSummary summary = TaskTransformer.fromTask(ui.getBaseUriBuilder()).apply(task);
+        return status(ACCEPTED).entity(summary).build();
+    }
+
+    @Override
+    public List<EntitySummary> getDescendants(String application, String entity, String typeRegex) {
+        return EntityTransformer.entitySummaries(brooklyn().descendantsOfType(application, entity, typeRegex), ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public Map<String, Object> getDescendantsSensor(String application, String entity, String sensor, String typeRegex) {
+        Iterable<Entity> descs = brooklyn().descendantsOfType(application, entity, typeRegex);
+        return ApplicationResource.getSensorMap(sensor, descs);
+    }
+
+    @Override
+    public List<LocationSummary> getLocations(String application, String entity) {
+        List<LocationSummary> result = Lists.newArrayList();
+        Entity e = brooklyn().getEntity(application, entity);
+        for (Location l : e.getLocations()) {
+            result.add(LocationTransformer.newInstance(mgmt(), l, LocationDetailLevel.NONE, ui.getBaseUriBuilder()));
+        }
+        return result;
+    }
+
+    @Override
+    public String getSpec(String applicationToken, String entityToken) {
+        Entity entity = brooklyn().getEntity(applicationToken, entityToken);
+        NamedStringTag spec = BrooklynTags.findFirst(BrooklynTags.YAML_SPEC_KIND, entity.tags().getTags());
+        if (spec == null)
+            return null;
+        return (String) WebResourceUtils.getValueForDisplay(spec.getContents(), true, true);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LocationResource.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LocationResource.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LocationResource.java
new file mode 100644
index 0000000..8f4251c
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/LocationResource.java
@@ -0,0 +1,186 @@
+/*
+ * 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.rest.resources;
+
+import java.net.URI;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationDefinition;
+import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.core.location.LocationConfigKeys;
+import org.apache.brooklyn.rest.api.LocationApi;
+import org.apache.brooklyn.rest.domain.LocationSpec;
+import org.apache.brooklyn.rest.domain.LocationSummary;
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+import org.apache.brooklyn.rest.transform.LocationTransformer;
+import org.apache.brooklyn.rest.transform.LocationTransformer.LocationDetailLevel;
+import org.apache.brooklyn.rest.util.EntityLocationUtils;
+import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.text.NaturalOrderComparator;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import static org.apache.brooklyn.rest.util.WebResourceUtils.serviceAbsoluteUriBuilder;
+
+@SuppressWarnings("deprecation")
+@HaHotStateRequired
+public class LocationResource extends AbstractBrooklynRestResource implements LocationApi {
+
+    private static final Logger log = LoggerFactory.getLogger(LocationResource.class);
+
+    private final Set<String> specsWarnedOnException = Sets.newConcurrentHashSet();
+
+    @Override
+    public List<LocationSummary> list() {
+        Function<LocationDefinition, LocationSummary> transformer = new Function<LocationDefinition, LocationSummary>() {
+            @Override
+            public LocationSummary apply(LocationDefinition l) {
+                try {
+                    return LocationTransformer.newInstance(mgmt(), l, LocationDetailLevel.LOCAL_EXCLUDING_SECRET, ui.getBaseUriBuilder());
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    String spec = l.getSpec();
+                    if (spec == null || specsWarnedOnException.add(spec)) {
+                        log.warn("Unable to find details of location {} in REST call to list (ignoring location): {}", l, e);
+                        if (log.isDebugEnabled()) log.debug("Error details for location " + l, e);
+                    } else {
+                        if (log.isTraceEnabled())
+                            log.trace("Unable again to find details of location {} in REST call to list (ignoring location): {}", l, e);
+                    }
+                    return null;
+                }
+            }
+        };
+        return FluentIterable.from(brooklyn().getLocationRegistry().getDefinedLocations().values())
+                .transform(transformer)
+                .filter(LocationSummary.class)
+                .toSortedList(nameOrSpecComparator());
+    }
+
+    private static NaturalOrderComparator COMPARATOR = new NaturalOrderComparator();
+    private static Comparator<LocationSummary> nameOrSpecComparator() {
+        return new Comparator<LocationSummary>() {
+            @Override
+            public int compare(LocationSummary o1, LocationSummary o2) {
+                return COMPARATOR.compare(getNameOrSpec(o1).toLowerCase(), getNameOrSpec(o2).toLowerCase());
+            }
+        };
+    }
+    private static String getNameOrSpec(LocationSummary o) {
+        if (Strings.isNonBlank(o.getName())) return o.getName();
+        if (Strings.isNonBlank(o.getSpec())) return o.getSpec();
+        return o.getId();
+    }
+
+    // this is here to support the web GUI's circles
+    @Override
+    public Map<String,Map<String,Object>> getLocatedLocations() {
+      Map<String,Map<String,Object>> result = new LinkedHashMap<String,Map<String,Object>>();
+      Map<Location, Integer> counts = new EntityLocationUtils(mgmt()).countLeafEntitiesByLocatedLocations();
+      for (Map.Entry<Location,Integer> count: counts.entrySet()) {
+          Location l = count.getKey();
+          Map<String,Object> m = MutableMap.<String,Object>of(
+                  "id", l.getId(),
+                  "name", l.getDisplayName(),
+                  "leafEntityCount", count.getValue(),
+                  "latitude", l.getConfig(LocationConfigKeys.LATITUDE),
+                  "longitude", l.getConfig(LocationConfigKeys.LONGITUDE)
+              );
+          result.put(l.getId(), m);
+      }
+      return result;
+    }
+
+    /** @deprecated since 0.7.0; REST call now handled by below (optional query parameter added) */
+    public LocationSummary get(String locationId) {
+        return get(locationId, false);
+    }
+
+    @Override
+    public LocationSummary get(String locationId, String fullConfig) {
+        return get(locationId, Boolean.valueOf(fullConfig));
+    }
+
+    public LocationSummary get(String locationId, boolean fullConfig) {
+        LocationDetailLevel configLevel = fullConfig ? LocationDetailLevel.FULL_EXCLUDING_SECRET : LocationDetailLevel.LOCAL_EXCLUDING_SECRET;
+        Location l1 = mgmt().getLocationManager().getLocation(locationId);
+        if (l1!=null) {
+            return LocationTransformer.newInstance(mgmt(), l1, configLevel, ui.getBaseUriBuilder());
+        }
+
+        LocationDefinition l2 = brooklyn().getLocationRegistry().getDefinedLocationById(locationId);
+        if (l2==null) throw WebResourceUtils.notFound("No location matching %s", locationId);
+        return LocationTransformer.newInstance(mgmt(), l2, configLevel, ui.getBaseUriBuilder());
+    }
+
+    @Override
+    public Response create(LocationSpec locationSpec) {
+        String name = locationSpec.getName();
+        ImmutableList.Builder<String> yaml = ImmutableList.<String>builder().add(
+                "brooklyn.catalog:",
+                "  symbolicName: "+name,
+                "",
+                "brooklyn.locations:",
+                "- type: "+locationSpec.getSpec());
+        if (locationSpec.getConfig().size() > 0) {
+            yaml.add("  brooklyn.config:");
+            for (Map.Entry<String, ?> entry : locationSpec.getConfig().entrySet()) {
+                yaml.add("    " + entry.getKey() + ": " + entry.getValue());
+            }
+        }
+
+        String locationBlueprint = Joiner.on("\n").join(yaml.build());
+        brooklyn().getCatalog().addItems(locationBlueprint);
+        LocationDefinition l = brooklyn().getLocationRegistry().getDefinedLocationByName(name);
+        URI ref = serviceAbsoluteUriBuilder(ui.getBaseUriBuilder(), LocationApi.class, "get").build(name);
+        return Response.created(ref)
+                .entity(LocationTransformer.newInstance(mgmt(), l, LocationDetailLevel.LOCAL_EXCLUDING_SECRET, ui.getBaseUriBuilder()))
+                .build();
+    }
+
+    @Override
+    @Deprecated
+    public void delete(String locationId) {
+        // TODO make all locations be part of the catalog, then flip the JS GUI to use catalog api
+        if (deleteAllVersions(locationId)>0) return;
+        throw WebResourceUtils.notFound("No catalog item location matching %s; only catalog item locations can be deleted", locationId);
+    }
+    
+    private int deleteAllVersions(String locationId) {
+        RegisteredType item = mgmt().getTypeRegistry().get(locationId);
+        if (item==null) return 0; 
+        brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion());
+        return 1 + deleteAllVersions(locationId);
+    }
+}