You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/12/23 12:06:27 UTC

[04/71] [abbrv] incubator-brooklyn git commit: Merge commit 'e430723' into reorg2

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
index 0000000,89f253a..22a4502
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/ApplicationResource.java
@@@ -1,0 -1,472 +1,480 @@@
+ /*
+  * 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 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.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.codehaus.jackson.JsonNode;
+ import org.codehaus.jackson.node.ArrayNode;
+ import org.codehaus.jackson.node.ObjectNode;
+ 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.Iterables;
+ 
+ @HaHotStateRequired
+ public class ApplicationResource extends AbstractBrooklynRestResource implements ApplicationApi {
+ 
+     private static final Logger log = LoggerFactory.getLogger(ApplicationResource.class);
+ 
+     @Context
+     private UriInfo uriInfo;
+ 
+     /** @deprecated since 0.6.0 use {@link #fetch(String)} (with slightly different, but better semantics) */
+     @Deprecated
+     @Override
+     public JsonNode applicationTree() {
+         ArrayNode apps = mapper().createArrayNode();
+         for (Application application : mgmt().getApplications())
+             apps.add(recursiveTreeFromEntity(application));
+         return apps;
+     }
+ 
+     private ObjectNode entityBase(Entity entity) {
+         ObjectNode aRoot = mapper().createObjectNode();
+         aRoot.put("name", entity.getDisplayName());
+         aRoot.put("id", entity.getId());
+         aRoot.put("type", entity.getEntityType().getName());
+ 
+         Boolean serviceUp = entity.getAttribute(Attributes.SERVICE_UP);
+         if (serviceUp!=null) aRoot.put("serviceUp", serviceUp);
+ 
+         Lifecycle serviceState = entity.getAttribute(Attributes.SERVICE_STATE_ACTUAL);
+         if (serviceState!=null) aRoot.put("serviceState", serviceState.toString());
+ 
+         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)+"/icon";
+             aRoot.put("iconUrl", iconUrl);
+         }
+ 
+         return aRoot;
+     }
+ 
+     private JsonNode recursiveTreeFromEntity(Entity entity) {
+         ObjectNode aRoot = entityBase(entity);
+ 
+         if (!entity.getChildren().isEmpty())
+             aRoot.put("children", childEntitiesRecursiveAsArray(entity));
+ 
+         return aRoot;
+     }
+ 
+     // TODO when applicationTree can be removed, replace this with an extension to EntitySummary (without links)
+     private JsonNode fromEntity(Entity entity) {
+         ObjectNode aRoot = entityBase(entity);
+ 
+         aRoot.put("applicationId", entity.getApplicationId());
+ 
+         if (entity.getParent()!=null) {
+             aRoot.put("parentId", entity.getParent().getId());
+         }
+ 
+         if (!entity.groups().isEmpty())
+             aRoot.put("groupIds", entitiesIdAsArray(entity.groups()));
+ 
+         if (!entity.getChildren().isEmpty())
+             aRoot.put("children", entitiesIdAndNameAsArray(entity.getChildren()));
+ 
+         if (entity instanceof Group) {
+             // use attribute instead of method in case it is read-only
+             Collection<Entity> members = entity.getAttribute(AbstractGroup.GROUP_MEMBERS);
+             if (members!=null && !members.isEmpty())
+                 aRoot.put("members", entitiesIdAndNameAsArray(members));
+         }
+ 
+         return aRoot;
+     }
+ 
+     private ArrayNode childEntitiesRecursiveAsArray(Entity entity) {
+         ArrayNode node = mapper().createArrayNode();
+         for (Entity e : entity.getChildren()) {
+             if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                 node.add(recursiveTreeFromEntity(e));
+             }
+         }
+         return node;
+     }
+ 
+     private ArrayNode entitiesIdAndNameAsArray(Collection<? extends Entity> entities) {
+         ArrayNode node = mapper().createArrayNode();
+         for (Entity entity : entities) {
+             if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                 ObjectNode holder = mapper().createObjectNode();
+                 holder.put("id", entity.getId());
+                 holder.put("name", entity.getDisplayName());
+                 node.add(holder);
+             }
+         }
+         return node;
+     }
+ 
+     private ArrayNode entitiesIdAsArray(Iterable<? extends Entity> entities) {
+         ArrayNode node = mapper().createArrayNode();
+         for (Entity entity : entities) {
+             if (Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_ENTITY, entity)) {
+                 node.add(entity.getId());
+             }
+         }
+         return node;
+     }
+ 
+     @Override
+     public JsonNode fetch(String entityIds) {
+         Map<String, JsonNode> jsonEntitiesById = MutableMap.of();
+         for (Application application : mgmt().getApplications())
+             jsonEntitiesById.put(application.getId(), 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)) {
+                         jsonEntitiesById.put(entity.getId(), fromEntity(entity));
+                     }
+                     entity = entity.getParent();
+                 }
+             }
+         }
+ 
+         ArrayNode result = mapper().createArrayNode();
+         for (JsonNode n: jsonEntitiesById.values()) result.add(n);
+         return result;
+     }
+ 
+     @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.FROM_APPLICATION)
+                 .toList();
+     }
+ 
+     @Override
+     public ApplicationSummary get(String application) {
+         return ApplicationTransformer.summaryFromApplication(brooklyn().getApplication(application));
+     }
+ 
+     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.unauthorized("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.FROM_TASK.apply(t);
+         URI ref = uriInfo.getBaseUriBuilder()
+                 .path(ApplicationApi.class)
+                 .path(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.unauthorized("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.unauthorized("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 = URI.create(app.getApplicationId());
+             ResponseBuilder response = created(ref);
+             if (result.task() != null)
+                 response.entity(TaskTransformer.FROM_TASK.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.unauthorized("User '%s' is not authorized to delete application %s",
+                 Entitlements.getEntitlementContext().user(), app);
+         }
+         Task<?> t = brooklyn().destroy(app);
+         TaskSummary ts = TaskTransformer.FROM_TASK.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) {
 -        if (RegisteredTypes.validate(mgmt().getTypeRegistry().get(type), RegisteredTypeLoadingContexts.spec(subType)) == null) {
 -            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);
++        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));
+     }
+ 
+     @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/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
index 0000000,ecbeffb..01bf992
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
@@@ -1,0 -1,508 +1,516 @@@
+ /*
+  * 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.io.InputStream;
+ 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.Consumes;
+ 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.guava.Maybe;
+ import org.apache.brooklyn.util.stream.Streams;
+ 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 com.sun.jersey.core.header.FormDataContentDisposition;
+ 
+ @HaHotStateRequired
+ public class CatalogResource extends AbstractBrooklynRestResource implements CatalogApi {
+ 
+     private static final Logger log = LoggerFactory.getLogger(CatalogResource.class);
+     
+     @SuppressWarnings("rawtypes")
+     private final Function<CatalogItem, CatalogItemSummary> TO_CATALOG_ITEM_SUMMARY = new Function<CatalogItem, CatalogItemSummary>() {
+         @Override
+         public CatalogItemSummary apply(@Nullable CatalogItem input) {
+             return CatalogTransformer.catalogItemSummary(brooklyn(), input);
+         }
+     };
+ 
+     @Override
+     @Consumes(MediaType.MULTIPART_FORM_DATA)
+     public Response createFromMultipart(InputStream uploadedInputStream, FormDataContentDisposition fileDetail) {
+       return create(Streams.readFullyString(uploadedInputStream));
+     }
+ 
+     static Set<String> missingIcons = MutableSet.of();
+     
+     @Override
+     public Response create(String yaml) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.ADD_CATALOG_ITEM, yaml)) {
+             throw WebResourceUtils.unauthorized("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));
+         }
+         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.unauthorized("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(String entityId) throws Exception {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.MODIFY_CATALOG_ITEM, StringAndArgument.of(entityId, "delete"))) {
+             throw WebResourceUtils.unauthorized("User '%s' is not authorized to modify catalog",
+                 Entitlements.getEntitlementContext().user());
+         }
+         try {
 -            RegisteredType item = RegisteredTypes.validate(mgmt().getTypeRegistry().get(entityId), RegisteredTypeLoadingContexts.spec(Entity.class));
 -            if (item==null) {
++            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);
+             }
 -            brooklyn().getCatalog().deleteCatalogItem(item.getSymbolicName(), item.getVersion());
++            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) {
 -            throw WebResourceUtils.notFound("Entity with id '%s' not found", entityId);
++            // 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.unauthorized("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.unauthorized("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.unauthorized("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(String entityId) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, entityId)) {
+             throw WebResourceUtils.unauthorized("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);
+     }
+     
+     @Override
+     public CatalogEntitySummary getEntity(String symbolicName, String version) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, symbolicName+(Strings.isBlank(version)?"":":"+version))) {
+             throw WebResourceUtils.unauthorized("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);
+     }
+ 
+     @Override
+     @Deprecated
+     public CatalogEntitySummary getApplication(String applicationId) throws Exception {
+         return getEntity(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(String policyId) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, policyId)) {
+             throw WebResourceUtils.unauthorized("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);
+     }
+ 
+     @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.unauthorized("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);
+     }
+ 
+     @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(String locationId) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, locationId)) {
+             throw WebResourceUtils.unauthorized("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);
+     }
+ 
+     @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.unauthorized("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);
+     }
+ 
+     @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, TO_CATALOG_ITEM_SUMMARY);
+     }
+ 
+     @Override
+     @Deprecated
+     public Response getIcon(String itemId) {
+         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), Entitlements.SEE_CATALOG_ITEM, itemId)) {
+             throw WebResourceUtils.unauthorized("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.unauthorized("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 \"/v1/catalog/entities/{itemId}/deprecated/{deprecated}\" for "+itemId
+                 +"; use \"/v1/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.unauthorized("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.unauthorized("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/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java
index 0000000,8b2b9da..11884c3
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java
@@@ -1,0 -1,165 +1,166 @@@
+ /*
+  * 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.security.provider;
+ 
+ import java.lang.reflect.Constructor;
+ import java.util.concurrent.atomic.AtomicLong;
+ 
+ import javax.servlet.http.HttpSession;
+ 
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ import org.apache.brooklyn.api.mgmt.ManagementContext;
+ import org.apache.brooklyn.config.StringConfigMap;
+ import org.apache.brooklyn.core.internal.BrooklynProperties;
++import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+ import org.apache.brooklyn.rest.BrooklynWebConfig;
+ import org.apache.brooklyn.util.text.Strings;
+ 
+ public class DelegatingSecurityProvider implements SecurityProvider {
+ 
+     private static final Logger log = LoggerFactory.getLogger(DelegatingSecurityProvider.class);
+     protected final ManagementContext mgmt;
+ 
+     public DelegatingSecurityProvider(ManagementContext mgmt) {
+         this.mgmt = mgmt;
+         mgmt.addPropertiesReloadListener(new PropertiesListener());
+     }
+     
+     private SecurityProvider delegate;
+     private final AtomicLong modCount = new AtomicLong();
+ 
+     private class PropertiesListener implements ManagementContext.PropertiesReloadListener {
+         private static final long serialVersionUID = 8148722609022378917L;
+ 
+         @Override
+         public void reloaded() {
+             log.debug("{} reloading security provider", DelegatingSecurityProvider.this);
+             synchronized (DelegatingSecurityProvider.this) {
+                 loadDelegate();
+                 invalidateExistingSessions();
+             }
+         }
+     }
+ 
+     public synchronized SecurityProvider getDelegate() {
+         if (delegate == null) {
+             delegate = loadDelegate();
+         }
+         return delegate;
+     }
+ 
+     @SuppressWarnings("unchecked")
+     private synchronized SecurityProvider loadDelegate() {
+         StringConfigMap brooklynProperties = mgmt.getConfig();
+ 
+         SecurityProvider presetDelegate = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE);
+         if (presetDelegate!=null) {
+             log.info("REST using pre-set security provider " + presetDelegate);
+             return presetDelegate;
+         }
+         
+         String className = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME);
+ 
+         if (delegate != null && BrooklynWebConfig.hasNoSecurityOptions(mgmt.getConfig())) {
+             log.debug("{} refusing to change from {}: No security provider set in reloaded properties.",
+                     this, delegate);
+             return delegate;
+         }
+         log.info("REST using security provider " + className);
+ 
+         try {
+             Class<? extends SecurityProvider> clazz;
+             try {
+                 clazz = (Class<? extends SecurityProvider>) Class.forName(className);
+             } catch (Exception e) {
+                 String oldPackage = "brooklyn.web.console.security.";
+                 if (className.startsWith(oldPackage)) {
+                     className = Strings.removeFromStart(className, oldPackage);
+                     className = DelegatingSecurityProvider.class.getPackage().getName() + "." + className;
+                     clazz = (Class<? extends SecurityProvider>) Class.forName(className);
+                     log.warn("Deprecated package " + oldPackage + " detected; please update security provider to point to " + className);
+                 } else throw e;
+             }
+ 
+             Constructor<? extends SecurityProvider> constructor;
+             try {
+                 constructor = clazz.getConstructor(ManagementContext.class);
+                 delegate = constructor.newInstance(mgmt);
+             } catch (Exception e) {
+                 constructor = clazz.getConstructor();
+                 Object delegateO = constructor.newInstance();
+                 if (!(delegateO instanceof SecurityProvider)) {
+                     // if classloaders get mangled it will be a different CL's SecurityProvider
+                     throw new ClassCastException("Delegate is either not a security provider or has an incompatible classloader: "+delegateO);
+                 }
+                 delegate = (SecurityProvider) delegateO;
+             }
+         } catch (Exception e) {
+             log.warn("REST unable to instantiate security provider " + className + "; all logins are being disallowed", e);
+             delegate = new BlackholeSecurityProvider();
+         }
+         
 -        ((BrooklynProperties)mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE, delegate);
++        ((ManagementContextInternal)mgmt).getBrooklynProperties().put(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE, delegate);
+         
+         return delegate;
+     }
+ 
+     /**
+      * Causes all existing sessions to be invalidated.
+      */
+     protected void invalidateExistingSessions() {
+         modCount.incrementAndGet();
+     }
+ 
+     @Override
+     public boolean isAuthenticated(HttpSession session) {
+         if (session == null) return false;
+         Object modCountWhenFirstAuthenticated = session.getAttribute(getModificationCountKey());
+         boolean authenticated = getDelegate().isAuthenticated(session) &&
+                 Long.valueOf(modCount.get()).equals(modCountWhenFirstAuthenticated);
+         return authenticated;
+     }
+ 
+     @Override
+     public boolean authenticate(HttpSession session, String user, String password) {
+         boolean authenticated = getDelegate().authenticate(session, user, password);
+         if (authenticated) {
+             session.setAttribute(getModificationCountKey(), modCount.get());
+         }
+         if (log.isTraceEnabled() && authenticated) {
+             log.trace("User {} authenticated with provider {}", user, getDelegate());
+         } else if (!authenticated && log.isDebugEnabled()) {
+             log.debug("Failed authentication for user {} with provider {}", user, getDelegate());
+         }
+         return authenticated;
+     }
+ 
+     @Override
+     public boolean logout(HttpSession session) { 
+         boolean logout = getDelegate().logout(session);
+         if (logout) {
+             session.removeAttribute(getModificationCountKey());
+         }
+         return logout;
+     }
+ 
+     private String getModificationCountKey() {
+         return getClass().getName() + ".ModCount";
+     }
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java
index 0000000,a0795cb..562c85b
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java
@@@ -1,0 -1,117 +1,118 @@@
+ /*
+  * 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.security.provider;
+ 
+ import java.util.LinkedHashSet;
+ import java.util.Set;
+ import java.util.StringTokenizer;
+ 
+ import javax.servlet.http.HttpSession;
+ 
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ import org.apache.brooklyn.api.mgmt.ManagementContext;
+ import org.apache.brooklyn.config.StringConfigMap;
+ import org.apache.brooklyn.core.internal.BrooklynProperties;
++import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+ import org.apache.brooklyn.rest.BrooklynWebConfig;
+ import org.apache.brooklyn.rest.security.PasswordHasher;
+ 
+ /**
+  * Security provider which validates users against passwords according to property keys,
+  * as set in {@link BrooklynWebConfig#USERS} and {@link BrooklynWebConfig#PASSWORD_FOR_USER(String)}
+  */
+ public class ExplicitUsersSecurityProvider extends AbstractSecurityProvider implements SecurityProvider {
+ 
+     public static final Logger LOG = LoggerFactory.getLogger(ExplicitUsersSecurityProvider.class);
+     
+     protected final ManagementContext mgmt;
+     private boolean allowAnyUserWithValidPass;
+     private Set<String> allowedUsers = null;
+ 
+     public ExplicitUsersSecurityProvider(ManagementContext mgmt) {
+         this.mgmt = mgmt;
+         initialize();
+     }
+ 
+     private synchronized void initialize() {
+         if (allowedUsers != null) return;
+ 
+         StringConfigMap properties = mgmt.getConfig();
+ 
+         allowedUsers = new LinkedHashSet<String>();
+         String users = properties.getConfig(BrooklynWebConfig.USERS);
+         if (users == null) {
+             LOG.warn("REST has no users configured; no one will be able to log in!");
+         } else if ("*".equals(users)) {
+             LOG.info("REST allowing any user (so long as valid password is set)");
+             allowAnyUserWithValidPass = true;
+         } else {
+             StringTokenizer t = new StringTokenizer(users, ",");
+             while (t.hasMoreElements()) {
+                 allowedUsers.add(("" + t.nextElement()).trim());
+             }
+             LOG.info("REST allowing users: " + allowedUsers);
+         }
+     }
+     
+     @Override
+     public boolean authenticate(HttpSession session, String user, String password) {
+         if (session==null || user==null) return false;
+         
+         if (!allowAnyUserWithValidPass) {
+             if (!allowedUsers.contains(user)) {
+                 LOG.debug("REST rejecting unknown user "+user);
+                 return false;                
+             }
+         }
+ 
+         if (checkExplicitUserPassword(mgmt, user, password)) {
+             return allow(session, user);
+         }
+         return false;
+     }
+ 
+     /** checks the supplied candidate user and password against the
+      * expect password (or SHA-256 + SALT thereof) defined as brooklyn properties.
+      */
+     public static boolean checkExplicitUserPassword(ManagementContext mgmt, String user, String password) {
 -        BrooklynProperties properties = (BrooklynProperties) mgmt.getConfig();
++        BrooklynProperties properties = ((ManagementContextInternal)mgmt).getBrooklynProperties();
+         String expectedPassword = properties.getConfig(BrooklynWebConfig.PASSWORD_FOR_USER(user));
+         String salt = properties.getConfig(BrooklynWebConfig.SALT_FOR_USER(user));
+         String expectedSha256 = properties.getConfig(BrooklynWebConfig.SHA256_FOR_USER(user));
+         
+         return checkPassword(password, expectedPassword, expectedSha256, salt);
+     }
+     /** 
+      * checks a candidate password against the expected credential defined for a given user.
+      * the expected credentials can be supplied as an expectedPassword OR as
+      * a combination of the SHA-256 hash of the expected password plus a defined salt.
+      * the combination of the SHA+SALT allows credentials to be supplied in a non-plaintext manner.
+      */
+     public static boolean checkPassword(String candidatePassword, String expectedPassword, String expectedPasswordSha256, String salt) {
+         if (expectedPassword != null) {
+             return expectedPassword.equals(candidatePassword);
+         } else if (expectedPasswordSha256 != null) {
+             String hashedCandidatePassword = PasswordHasher.sha256(salt, candidatePassword);
+             return expectedPasswordSha256.equals(hashedCandidatePassword);
+         }
+ 
+         return false;
+     }
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
index 0000000,0f710bc..74625fd
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/CatalogTransformer.java
@@@ -1,0 -1,184 +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.transform;
+ 
+ import java.net.URI;
+ import java.util.Map;
+ import java.util.Set;
++import java.util.concurrent.atomic.AtomicInteger;
+ 
+ import org.apache.brooklyn.api.catalog.CatalogItem;
+ import org.apache.brooklyn.api.catalog.CatalogItem.CatalogItemType;
+ import org.apache.brooklyn.api.effector.Effector;
+ import org.apache.brooklyn.api.entity.Entity;
+ import org.apache.brooklyn.api.entity.EntitySpec;
+ import org.apache.brooklyn.api.entity.EntityType;
+ import org.apache.brooklyn.api.location.Location;
+ import org.apache.brooklyn.api.location.LocationSpec;
+ import org.apache.brooklyn.api.objs.SpecParameter;
+ import org.apache.brooklyn.api.policy.Policy;
+ import org.apache.brooklyn.api.policy.PolicySpec;
+ import org.apache.brooklyn.api.sensor.Sensor;
+ import org.apache.brooklyn.core.entity.EntityDynamicType;
+ import org.apache.brooklyn.core.mgmt.BrooklynTags;
+ import org.apache.brooklyn.core.objs.BrooklynTypes;
+ 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.domain.EffectorSummary;
+ import org.apache.brooklyn.rest.domain.EntityConfigSummary;
+ import org.apache.brooklyn.rest.domain.LocationConfigSummary;
+ import org.apache.brooklyn.rest.domain.PolicyConfigSummary;
+ import org.apache.brooklyn.rest.domain.SensorSummary;
+ import org.apache.brooklyn.rest.domain.SummaryComparators;
+ import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
+ import org.apache.brooklyn.util.collections.MutableMap;
+ import org.apache.brooklyn.util.collections.MutableSet;
+ import org.apache.brooklyn.util.exceptions.Exceptions;
+ import org.apache.brooklyn.util.javalang.Reflections;
+ import org.slf4j.LoggerFactory;
+ 
+ import com.google.common.collect.ImmutableSet;
+ import com.google.common.collect.Sets;
+ 
+ public class CatalogTransformer {
+ 
+     private static final org.slf4j.Logger log = LoggerFactory.getLogger(CatalogTransformer.class);
+     
+     public static <T extends Entity> CatalogEntitySummary catalogEntitySummary(BrooklynRestResourceUtils b, CatalogItem<T,EntitySpec<? extends T>> item) {
+         Set<EntityConfigSummary> config = Sets.newLinkedHashSet();
+         Set<SensorSummary> sensors = Sets.newTreeSet(SummaryComparators.nameComparator());
+         Set<EffectorSummary> effectors = Sets.newTreeSet(SummaryComparators.nameComparator());
+ 
+         EntitySpec<?> spec = null;
+ 
+         try {
+             spec = (EntitySpec<?>) b.getCatalog().createSpec((CatalogItem) item);
+             EntityDynamicType typeMap = BrooklynTypes.getDefinedEntityType(spec.getType());
+             EntityType type = typeMap.getSnapshot();
+ 
++            AtomicInteger paramPriorityCnt = new AtomicInteger();
+             for (SpecParameter<?> input: spec.getParameters())
 -                config.add(EntityTransformer.entityConfigSummary(input));
++                config.add(EntityTransformer.entityConfigSummary(input, paramPriorityCnt));
+             for (Sensor<?> x: type.getSensors())
+                 sensors.add(SensorTransformer.sensorSummaryForCatalog(x));
+             for (Effector<?> x: type.getEffectors())
+                 effectors.add(EffectorTransformer.effectorSummaryForCatalog(x));
+ 
+         } catch (Exception e) {
+             Exceptions.propagateIfFatal(e);
+             
+             // templates with multiple entities can't have spec created in the manner above; just ignore
+             if (item.getCatalogItemType()==CatalogItemType.ENTITY) {
+                 log.warn("Unable to create spec for "+item+": "+e, e);
+             }
+             if (log.isTraceEnabled()) {
+                 log.trace("Unable to create spec for "+item+": "+e, e);
+             }
+         }
+         
+         return new CatalogEntitySummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(),
+             item.getJavaType(), item.getPlanYaml(),
+             item.getDescription(), tidyIconLink(b, item, item.getIconUrl()),
+             makeTags(spec, item), config, sensors, effectors,
+             item.isDeprecated(), makeLinks(item));
+     }
+ 
+     @SuppressWarnings({ "unchecked", "rawtypes" })
+     public static CatalogItemSummary catalogItemSummary(BrooklynRestResourceUtils b, CatalogItem item) {
+         try {
+             switch (item.getCatalogItemType()) {
+             case TEMPLATE:
+             case ENTITY:
+                 return catalogEntitySummary(b, item);
+             case POLICY:
+                 return catalogPolicySummary(b, item);
+             case LOCATION:
+                 return catalogLocationSummary(b, item);
+             default:
+                 log.warn("Unexpected catalog item type when getting self link (supplying generic item): "+item.getCatalogItemType()+" "+item);
+             }
+         } catch (Exception e) {
+             Exceptions.propagateIfFatal(e);
+             log.warn("Invalid item in catalog when converting REST summaries (supplying generic item), at "+item+": "+e, e);
+         }
+         return new CatalogItemSummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(),
+             item.getJavaType(), item.getPlanYaml(),
+             item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), item.tags().getTags(), item.isDeprecated(), makeLinks(item));
+     }
+ 
+     public static CatalogPolicySummary catalogPolicySummary(BrooklynRestResourceUtils b, CatalogItem<? extends Policy,PolicySpec<?>> item) {
+         Set<PolicyConfigSummary> config = ImmutableSet.of();
+         return new CatalogPolicySummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(),
+                 item.getJavaType(), item.getPlanYaml(),
+                 item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), config,
+                 item.tags().getTags(), item.isDeprecated(), makeLinks(item));
+     }
+ 
+     public static CatalogLocationSummary catalogLocationSummary(BrooklynRestResourceUtils b, CatalogItem<? extends Location,LocationSpec<?>> item) {
+         Set<LocationConfigSummary> config = ImmutableSet.of();
+         return new CatalogLocationSummary(item.getSymbolicName(), item.getVersion(), item.getDisplayName(),
+                 item.getJavaType(), item.getPlanYaml(),
+                 item.getDescription(), tidyIconLink(b, item, item.getIconUrl()), config,
+                 item.tags().getTags(), item.isDeprecated(), makeLinks(item));
+     }
+ 
+     protected static Map<String, URI> makeLinks(CatalogItem<?,?> item) {
+         return MutableMap.<String, URI>of().addIfNotNull("self", URI.create(getSelfLink(item)));
+     }
+ 
+     protected static String getSelfLink(CatalogItem<?,?> item) {
+         String itemId = item.getId();
+         switch (item.getCatalogItemType()) {
+         case TEMPLATE:
+             return "/v1/applications/" + itemId + "/" + item.getVersion();
+         case ENTITY:
+             return "/v1/entities/" + itemId + "/" + item.getVersion();
+         case POLICY:
+             return "/v1/policies/" + itemId + "/" + item.getVersion();
+         case LOCATION:
+             return "/v1/locations/" + itemId + "/" + item.getVersion();
+         default:
+             log.warn("Unexpected catalog item type when getting self link (not supplying self link): "+item.getCatalogItemType()+" "+item);
+             return null;
+         }
+     }
+     private static String tidyIconLink(BrooklynRestResourceUtils b, CatalogItem<?,?> item, String iconUrl) {
+         if (b.isUrlServerSideAndSafe(iconUrl))
+             return "/v1/catalog/icon/"+item.getSymbolicName() + "/" + item.getVersion();
+         return iconUrl;
+     }
+ 
+     private static Set<Object> makeTags(EntitySpec<?> spec, CatalogItem<?, ?> item) {
+         // Combine tags on item with an InterfacesTag.
+         Set<Object> tags = MutableSet.copyOf(item.tags().getTags());
+         if (spec != null) {
+             Class<?> type;
+             if (spec.getImplementation() != null) {
+                 type = spec.getImplementation();
+             } else {
+                 type = spec.getType();
+             }
+             if (type != null) {
+                 tags.add(new BrooklynTags.TraitsTag(Reflections.getAllInterfaces(type)));
+             }
+         }
+         return tags;
+     }
+     
+ }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/018a0e15/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
----------------------------------------------------------------------
diff --cc brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
index 0000000,803acd9..2d9f8a0
mode 000000,100644..100644
--- a/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
+++ b/brooklyn-server/rest/rest-server/src/main/java/org/apache/brooklyn/rest/transform/EntityTransformer.java
@@@ -1,0 -1,161 +1,165 @@@
+ /*
+  * 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.transform;
+ 
+ import static com.google.common.collect.Iterables.transform;
+ 
+ import java.lang.reflect.Field;
+ import java.net.URI;
+ import java.util.List;
+ import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
+ 
+ import org.apache.brooklyn.api.catalog.CatalogConfig;
+ import org.apache.brooklyn.api.entity.Application;
+ import org.apache.brooklyn.api.entity.Entity;
+ import org.apache.brooklyn.api.objs.SpecParameter;
+ import org.apache.brooklyn.config.ConfigKey;
+ import org.apache.brooklyn.core.config.render.RendererHints;
+ import org.apache.brooklyn.rest.domain.EntityConfigSummary;
+ import org.apache.brooklyn.rest.domain.EntitySummary;
+ import org.apache.brooklyn.rest.util.WebResourceUtils;
+ import org.apache.brooklyn.util.collections.MutableMap;
+ import org.apache.brooklyn.util.net.URLParamEncoder;
+ 
+ import com.google.common.base.Function;
+ import com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.Iterables;
+ import com.google.common.collect.Lists;
+ 
+ /**
+  * @author Adam Lowe
+  */
+ public class EntityTransformer {
+ 
+     public static final Function<? super Entity, EntitySummary> FROM_ENTITY = new Function<Entity, EntitySummary>() {
+         @Override
+         public EntitySummary apply(Entity entity) {
+             return EntityTransformer.entitySummary(entity);
+         }
+     };
+ 
+     public static EntitySummary entitySummary(Entity entity) {
+         String applicationUri = "/v1/applications/" + entity.getApplicationId();
+         String entityUri = applicationUri + "/entities/" + entity.getId();
+         ImmutableMap.Builder<String, URI> lb = ImmutableMap.<String, URI>builder()
+                 .put("self", URI.create(entityUri));
+         if (entity.getParent()!=null)
+             lb.put("parent", URI.create(applicationUri+"/entities/"+entity.getParent().getId()));
+         String type = entity.getEntityType().getName();
+         lb.put("application", URI.create(applicationUri))
+                 .put("children", URI.create(entityUri + "/children"))
+                 .put("config", URI.create(entityUri + "/config"))
+                 .put("sensors", URI.create(entityUri + "/sensors"))
+                 .put("effectors", URI.create(entityUri + "/effectors"))
+                 .put("policies", URI.create(entityUri + "/policies"))
+                 .put("activities", URI.create(entityUri + "/activities"))
+                 .put("locations", URI.create(entityUri + "/locations"))
+                 .put("tags", URI.create(entityUri + "/tags"))
+                 .put("expunge", URI.create(entityUri + "/expunge"))
+                 .put("rename", URI.create(entityUri + "/name"))
+                 .put("spec", URI.create(entityUri + "/spec"))
+             ;
+ 
+         if (entity.getCatalogItemId() != null) {
+             lb.put("catalog", URI.create("/v1/catalog/entities/" + WebResourceUtils.getPathFromVersionedId(entity.getCatalogItemId())));
+         }
+ 
+         if (entity.getIconUrl()!=null)
+             lb.put("iconUrl", URI.create(entityUri + "/icon"));
+ 
+         return new EntitySummary(entity.getId(), entity.getDisplayName(), type, entity.getCatalogItemId(), lb.build());
+     }
+ 
+     public static List<EntitySummary> entitySummaries(Iterable<? extends Entity> entities) {
+         return Lists.newArrayList(transform(
+             entities,
+             new Function<Entity, EntitySummary>() {
+                 @Override
+                 public EntitySummary apply(Entity entity) {
+                     return EntityTransformer.entitySummary(entity);
+                 }
+             }));
+     }
+ 
+     protected static EntityConfigSummary entityConfigSummary(ConfigKey<?> config, String label, Double priority, Map<String, URI> links) {
+         Map<String, URI> mapOfLinks =  links==null ? null : ImmutableMap.copyOf(links);
+         return new EntityConfigSummary(config, label, priority, mapOfLinks);
+     }
+     /** generates a representation for a given config key, 
+      * with label inferred from annoation in the entity class,
+      * and links pointing to the entity and the applicaiton */
+     public static EntityConfigSummary entityConfigSummary(Entity entity, ConfigKey<?> config) {
+       /*
+        * following code nearly there to get the @CatalogConfig annotation
+        * in the class and use that to populate a label
+        */
+ 
+ //    EntityDynamicType typeMap = 
+ //            ((AbstractEntity)entity).getMutableEntityType();
+ //      // above line works if we can cast; line below won't work, but there should some way
+ //      // to get back the handle to the spec from an entity local, which then *would* work
+ //            EntityTypes.getDefinedEntityType(entity.getClass());
+ 
+ //    String label = typeMap.getConfigKeyField(config.getName());
+         String label = null;
+         Double priority = null;
+ 
+         String applicationUri = "/v1/applications/" + entity.getApplicationId();
+         String entityUri = applicationUri + "/entities/" + entity.getId();
+         String selfUri = entityUri + "/config/" + URLParamEncoder.encode(config.getName());
+         
+         MutableMap.Builder<String, URI> lb = MutableMap.<String, URI>builder()
+             .put("self", URI.create(selfUri))
+             .put("application", URI.create(applicationUri))
+             .put("entity", URI.create(entityUri))
+             .put("action:json", URI.create(selfUri));
+ 
+         Iterable<RendererHints.NamedAction> hints = Iterables.filter(RendererHints.getHintsFor(config), RendererHints.NamedAction.class);
+         for (RendererHints.NamedAction na : hints) {
+             SensorTransformer.addNamedAction(lb, na, entity.getConfig(config), config, entity);
+         }
+     
+         return entityConfigSummary(config, label, priority, lb.build());
+     }
+ 
+     public static String applicationUri(Application entity) {
+         return "/v1/applications/" + entity.getApplicationId();
+     }
+     
+     public static String entityUri(Entity entity) {
+         return applicationUri(entity.getApplication()) + "/entities/" + entity.getId();
+     }
+     
+     public static EntityConfigSummary entityConfigSummary(ConfigKey<?> config, Field configKeyField) {
+         CatalogConfig catalogConfig = configKeyField!=null ? configKeyField.getAnnotation(CatalogConfig.class) : null;
+         String label = catalogConfig==null ? null : catalogConfig.label();
+         Double priority = catalogConfig==null ? null : catalogConfig.priority();
+         return entityConfigSummary(config, label, priority, null);
+     }
+ 
 -    public static EntityConfigSummary entityConfigSummary(SpecParameter<?> input) {
 -        Double priority = input.isPinned() ? Double.valueOf(1d) : null;
++    public static EntityConfigSummary entityConfigSummary(SpecParameter<?> input, AtomicInteger paramPriorityCnt) {
++        // Increment the priority because the config container is a set. Server-side we are using an ordered set
++        // which results in correctly ordered items on the wire (as a list). Clients which use the java bindings
++        // though will push the items in an unordered set - so give them means to recover the correct order.
++        Double priority = input.isPinned() ? Double.valueOf(paramPriorityCnt.incrementAndGet()) : null;
+         return entityConfigSummary(input.getType(), input.getLabel(), priority, null);
+     }
+ 
+ }