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:33 UTC

[15/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/util/BrooklynRestResourceUtils.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtils.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtils.java
new file mode 100644
index 0000000..3f837a1
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtils.java
@@ -0,0 +1,609 @@
+/*
+ * 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.util;
+
+import static com.google.common.collect.Iterables.transform;
+import static org.apache.brooklyn.rest.util.WebResourceUtils.notFound;
+
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.ws.rs.core.MediaType;
+
+import org.apache.brooklyn.api.catalog.BrooklynCatalog;
+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.location.Location;
+import org.apache.brooklyn.api.location.LocationRegistry;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.camp.brooklyn.BrooklynCampConstants;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent;
+import org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent.Scope;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.catalog.CatalogPredicates;
+import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator;
+import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.objs.BrooklynTypes;
+import org.apache.brooklyn.core.typereg.RegisteredTypes;
+import org.apache.brooklyn.enricher.stock.Enrichers;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+import org.apache.brooklyn.rest.domain.ApplicationSpec;
+import org.apache.brooklyn.rest.domain.EntitySpec;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.javalang.Reflections;
+import org.apache.brooklyn.util.net.Urls;
+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.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.Files;
+
+public class BrooklynRestResourceUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynRestResourceUtils.class);
+
+    private final ManagementContext mgmt;
+    
+    public BrooklynRestResourceUtils(ManagementContext mgmt) {
+        Preconditions.checkNotNull(mgmt, "mgmt");
+        this.mgmt = mgmt;
+    }
+
+    public BrooklynCatalog getCatalog() {
+        return mgmt.getCatalog();
+    }
+    
+    public ClassLoader getCatalogClassLoader() {
+        return mgmt.getCatalogClassLoader();
+    }
+    
+    public LocationRegistry getLocationRegistry() {
+        return mgmt.getLocationRegistry();
+    }
+
+    /** finds the policy indicated by the given ID or name.
+     * @see {@link #getEntity(String,String)}; it then searches the policies of that
+     * entity for one whose ID or name matches that given.
+     * <p>
+     * 
+     * @throws 404 or 412 (unless input is null in which case output is null) */
+    public Policy getPolicy(String application, String entity, String policy) {
+        return getPolicy(getEntity(application, entity), policy);
+    }
+
+    /** finds the policy indicated by the given ID or name.
+     * @see {@link #getPolicy(String,String,String)}.
+     * <p>
+     * 
+     * @throws 404 or 412 (unless input is null in which case output is null) */
+    public Policy getPolicy(Entity entity, String policy) {
+        if (policy==null) return null;
+
+        for (Policy p: entity.policies()) {
+            if (policy.equals(p.getId())) return p;
+        }
+        for (Policy p: entity.policies()) {
+            if (policy.equals(p.getDisplayName())) return p;
+        }
+        
+        throw WebResourceUtils.notFound("Cannot find policy '%s' in entity '%s'", policy, entity);
+    }
+
+    /** finds the entity indicated by the given ID or name
+     * <p>
+     * prefers ID based lookup in which case appId is optional, and if supplied will be enforced.
+     * optionally the name can be supplied, for cases when paths should work across versions,
+     * in which case names will be searched recursively (and the application is required). 
+     * 
+     * @throws 404 or 412 (unless input is null in which case output is null) */
+    public Entity getEntity(String application, String entity) {
+        if (entity==null) return null;
+        Application app = application!=null ? getApplication(application) : null;
+        Entity e = mgmt.getEntityManager().getEntity(entity);
+        
+        if (e!=null) {
+            if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
+                throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
+            }
+            
+            if (app==null || app.equals(findTopLevelApplication(e))) return e;
+            throw WebResourceUtils.preconditionFailed("Application '%s' specified does not match application '%s' to which entity '%s' (%s) is associated", 
+                    application, e.getApplication()==null ? null : e.getApplication().getId(), entity, e);
+        }
+        if (application==null)
+            throw WebResourceUtils.notFound("Cannot find entity '%s': no known ID and application not supplied for searching", entity);
+        
+        assert app!=null : "null app should not be returned from getApplication";
+        e = searchForEntityNamed(app, entity);
+        if (e!=null) return e;
+        throw WebResourceUtils.notFound("Cannot find entity '%s' in application '%s' (%s)", entity, application, app);
+    }
+    
+    private Application findTopLevelApplication(Entity e) {
+        // For nested apps, e.getApplication() can return its direct parent-app rather than the root app
+        // (particularly if e.getApplication() was called before the parent-app was wired up to its parent,
+        // because that call causes the application to be cached).
+        // Therefore we continue to walk the hierarchy until we find an "orphaned" application at the top.
+        
+        Application app = e.getApplication();
+        while (app != null && !app.equals(app.getApplication())) {
+            app = app.getApplication();
+        }
+        return app;
+    }
+
+    /** looks for the given application instance, first by ID then by name
+     * 
+     * @throws 404 if not found, or not entitled
+     */
+    public Application getApplication(String application) {
+        Entity e = mgmt.getEntityManager().getEntity(application);
+        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.SEE_ENTITY, e)) {
+            throw notFound("Application '%s' not found", application);
+        }
+        
+        if (e != null && e instanceof Application) return (Application) e;
+        for (Application app : mgmt.getApplications()) {
+            if (app.getId().equals(application)) return app;
+            if (application.equalsIgnoreCase(app.getDisplayName())) return app;
+        }
+        
+        throw notFound("Application '%s' not found", application);
+    }
+
+    /** walks the hierarchy (depth-first) at root (often an Application) looking for
+     * an entity matching the given ID or name; returns the first such entity, or null if none found
+     **/
+    public Entity searchForEntityNamed(Entity root, String entity) {
+        if (root.getId().equals(entity) || entity.equals(root.getDisplayName())) return root;
+        for (Entity child: root.getChildren()) {
+            Entity result = searchForEntityNamed(child, entity);
+            if (result!=null) return result;
+        }
+        return null;
+    }
+
+    private class FindItemAndClass {
+        String catalogItemId;
+        Class<? extends Entity> clazz;
+        
+        @SuppressWarnings({ "unchecked" })
+        private FindItemAndClass inferFrom(String type) {
+            RegisteredType item = mgmt.getTypeRegistry().get(type);
+            if (item==null) {
+                // deprecated attempt to load an item not in the type registry
+                
+                // although the method called was deprecated in 0.7.0, its use here was not warned until 0.9.0;
+                // therefore this behaviour should not be changed until after 0.9.0;
+                // at which point it should try a pojo load (see below)
+                item = getCatalogItemForType(type);
+                if (item!=null) {
+                    log.warn("Creating application for requested type `"+type+" using item "+item+"; "
+                        + "the registered type name ("+item.getSymbolicName()+") should be used from the spec instead, "
+                        + "or the type registered under its own name. "
+                        + "Future versions will likely change semantics to attempt a POJO load of the type instead.");
+                }
+            }
+            
+            if (item != null) {
+                return setAs(
+                    mgmt.getTypeRegistry().createSpec(item, null, org.apache.brooklyn.api.entity.EntitySpec.class).getType(),
+                    item.getId());
+            } else {
+                try {
+                    setAs(
+                        (Class<? extends Entity>) getCatalog().getRootClassLoader().loadClass(type),
+                        null);
+                    log.info("Catalog does not contain item for type {}; loaded class directly instead", type);
+                    return this;
+                } catch (ClassNotFoundException e2) {
+                    log.warn("No catalog item for type {}, and could not load class directly; rethrowing", type);
+                    throw new NoSuchElementException("Unable to find catalog item for type "+type);
+                }
+            }
+        }
+
+        private FindItemAndClass setAs(Class<? extends Entity> clazz, String catalogItemId) {
+            this.clazz = clazz;
+            this.catalogItemId = catalogItemId;
+            return this;
+        }
+        
+        @Deprecated // see caller
+        private RegisteredType getCatalogItemForType(String typeName) {
+            final RegisteredType resultI;
+            if (CatalogUtils.looksLikeVersionedId(typeName)) {
+                //All catalog identifiers of the form xxxx:yyyy are composed of symbolicName+version.
+                //No javaType is allowed as part of the identifier.
+                resultI = mgmt.getTypeRegistry().get(typeName);
+            } else {
+                //Usually for catalog items with javaType (that is items from catalog.xml)
+                //the symbolicName and javaType match because symbolicName (was ID)
+                //is not specified explicitly. But could be the case that there is an item
+                //whose symbolicName is explicitly set to be different from the javaType.
+                //Note that in the XML the attribute is called registeredTypeName.
+                Iterable<CatalogItem<Object,Object>> resultL = mgmt.getCatalog().getCatalogItems(CatalogPredicates.javaType(Predicates.equalTo(typeName)));
+                if (!Iterables.isEmpty(resultL)) {
+                    //Push newer versions in front of the list (not that there should
+                    //be more than one considering the items are coming from catalog.xml).
+                    resultI = RegisteredTypes.of(sortVersionsDesc(resultL).iterator().next());
+                    if (log.isDebugEnabled() && Iterables.size(resultL)>1) {
+                        log.debug("Found "+Iterables.size(resultL)+" matches in catalog for type "+typeName+"; returning the result with preferred version, "+resultI);
+                    }
+                } else {
+                    //As a last resort try searching for items with the same symbolicName supposedly
+                    //different from the javaType.
+                    resultI = mgmt.getTypeRegistry().get(typeName, BrooklynCatalog.DEFAULT_VERSION);
+                    if (resultI != null) {
+                        if (resultI.getSuperTypes().isEmpty()) {
+                            //Catalog items scanned from the classpath (using reflection and annotations) now
+                            //get yaml spec rather than a java type. Can't use those when creating apps from
+                            //the legacy app spec format.
+                            log.warn("Unable to find catalog item for type "+typeName +
+                                    ". There is an existing catalog item with ID " + resultI.getId() +
+                                    " but it doesn't define a class type.");
+                            return null;
+                        }
+                    }
+                }
+            }
+            return resultI;
+        }
+        private <T,SpecT> Collection<CatalogItem<T,SpecT>> sortVersionsDesc(Iterable<CatalogItem<T,SpecT>> versions) {
+            return ImmutableSortedSet.orderedBy(CatalogItemComparator.<T,SpecT>getInstance()).addAll(versions).build();
+        }
+    }
+    
+    @SuppressWarnings({ "deprecation" })
+    public Application create(ApplicationSpec spec) {
+        log.warn("Using deprecated functionality (as of 0.9.0), ApplicationSpec style (pre CAMP plans). " +
+                    "Transition to actively supported spec plans.");
+        log.debug("REST creating application instance for {}", spec);
+        
+        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.DEPLOY_APPLICATION, spec)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to deploy application %s",
+                Entitlements.getEntitlementContext().user(), spec);
+        }
+        
+        final String type = spec.getType();
+        final String name = spec.getName();
+        final Map<String,String> configO = spec.getConfig();
+        final Set<EntitySpec> entities = (spec.getEntities() == null) ? ImmutableSet.<EntitySpec>of() : spec.getEntities();
+        
+        final Application instance;
+
+        // Load the class; first try to use the appropriate catalog item; but then allow anything that is on the classpath
+        FindItemAndClass itemAndClass;
+        if (Strings.isEmpty(type)) {
+            itemAndClass = new FindItemAndClass().setAs(BasicApplication.class, null);
+        } else {
+            itemAndClass = new FindItemAndClass().inferFrom(type);
+        }
+        
+        if (!Entitlements.isEntitled(mgmt.getEntitlementManager(), Entitlements.INVOKE_EFFECTOR, null)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to create application from applicationSpec %s",
+                Entitlements.getEntitlementContext().user(), spec);
+        }
+
+        try {
+            if (org.apache.brooklyn.core.entity.factory.ApplicationBuilder.class.isAssignableFrom(itemAndClass.clazz)) {
+                // warning only added in 0.9.0
+                log.warn("Using deprecated ApplicationBuilder "+itemAndClass.clazz+"; callers must migrate to use of Application");
+                Constructor<?> constructor = itemAndClass.clazz.getConstructor();
+                org.apache.brooklyn.core.entity.factory.ApplicationBuilder appBuilder = (org.apache.brooklyn.core.entity.factory.ApplicationBuilder) constructor.newInstance();
+                if (!Strings.isEmpty(name)) appBuilder.appDisplayName(name);
+                if (entities.size() > 0)
+                    log.warn("Cannot supply additional entities when using an ApplicationBuilder; ignoring in spec {}", spec);
+
+                log.info("REST placing '{}' under management", spec.getName());
+                appBuilder.configure(convertFlagsToKeys(appBuilder.getType(), configO));
+                configureRenderingMetadata(spec, appBuilder);
+                instance = appBuilder.manage(mgmt);
+
+            } else if (Application.class.isAssignableFrom(itemAndClass.clazz)) {
+                org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId);
+                configureRenderingMetadata(spec, coreSpec);
+                for (EntitySpec entitySpec : entities) {
+                    log.info("REST creating instance for entity {}", entitySpec.getType());
+                    coreSpec.child(toCoreEntitySpec(entitySpec));
+                }
+
+                log.info("REST placing '{}' under management", spec.getName() != null ? spec.getName() : spec);
+                instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);
+
+            } else if (Entity.class.isAssignableFrom(itemAndClass.clazz)) {
+                if (entities.size() > 0)
+                    log.warn("Cannot supply additional entities when using a non-application entity; ignoring in spec {}", spec);
+
+                org.apache.brooklyn.api.entity.EntitySpec<?> coreSpec = toCoreEntitySpec(BasicApplication.class, name, configO, itemAndClass.catalogItemId);
+                configureRenderingMetadata(spec, coreSpec);
+
+                coreSpec.child(toCoreEntitySpec(itemAndClass.clazz, name, configO, itemAndClass.catalogItemId)
+                    .configure(BrooklynCampConstants.PLAN_ID, "soleChildId"));
+                coreSpec.enricher(Enrichers.builder()
+                    .propagatingAllBut(Attributes.SERVICE_UP, Attributes.SERVICE_NOT_UP_INDICATORS, 
+                        Attributes.SERVICE_STATE_ACTUAL, Attributes.SERVICE_STATE_EXPECTED, 
+                        Attributes.SERVICE_PROBLEMS)
+                        .from(new DslComponent(Scope.CHILD, "soleChildId").newTask())
+                        .build());
+
+                log.info("REST placing '{}' under management", spec.getName());
+                instance = (Application) mgmt.getEntityManager().createEntity(coreSpec);
+
+            } else {
+                throw new IllegalArgumentException("Class " + itemAndClass.clazz + " must extend one of ApplicationBuilder, Application or Entity");
+            }
+
+            return instance;
+
+        } catch (Exception e) {
+            log.error("REST failed to create application: " + e, e);
+            throw Exceptions.propagate(e);
+        }
+    }
+    
+    public Task<?> start(Application app, ApplicationSpec spec) {
+        return start(app, getLocations(spec));
+    }
+
+    public Task<?> start(Application app, List<? extends Location> locations) {
+        return Entities.invokeEffector(app, app, Startable.START,
+                MutableMap.of("locations", locations));
+    }
+
+    public List<Location> getLocations(ApplicationSpec spec) {
+        // Start all the managed entities by asking the app instance to start in background
+        Function<String, Location> buildLocationFromId = new Function<String, Location>() {
+            @Override
+            public Location apply(String id) {
+                id = fixLocation(id);
+                return getLocationRegistry().resolve(id);
+            }
+        };
+
+        ArrayList<Location> locations = Lists.newArrayList(transform(spec.getLocations(), buildLocationFromId));
+        return locations;
+    }
+
+    private org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> toCoreEntitySpec(org.apache.brooklyn.rest.domain.EntitySpec spec) {
+        String type = spec.getType();
+        String name = spec.getName();
+        Map<String, String> config = (spec.getConfig() == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(spec.getConfig());
+
+        FindItemAndClass itemAndClass = new FindItemAndClass().inferFrom(type);
+        
+        final Class<? extends Entity> clazz = itemAndClass.clazz;
+        org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
+        if (clazz.isInterface()) {
+            result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
+        } else {
+            result = org.apache.brooklyn.api.entity.EntitySpec.create(Entity.class).impl(clazz).additionalInterfaces(Reflections.getAllInterfaces(clazz));
+        }
+        result.catalogItemId(itemAndClass.catalogItemId);
+        if (!Strings.isEmpty(name)) result.displayName(name);
+        result.configure( convertFlagsToKeys(result.getType(), config) );
+        configureRenderingMetadata(spec, result);
+        return result;
+    }
+    
+    @SuppressWarnings("deprecation")
+    protected void configureRenderingMetadata(ApplicationSpec spec, org.apache.brooklyn.core.entity.factory.ApplicationBuilder appBuilder) {
+        appBuilder.configure(getRenderingConfigurationFor(spec.getType()));
+    }
+
+    protected void configureRenderingMetadata(ApplicationSpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
+        entity.configure(getRenderingConfigurationFor(input.getType()));
+    }
+
+    protected void configureRenderingMetadata(EntitySpec input, org.apache.brooklyn.api.entity.EntitySpec<?> entity) {
+        entity.configure(getRenderingConfigurationFor(input.getType()));
+    }
+
+    protected Map<?, ?> getRenderingConfigurationFor(String catalogId) {
+        MutableMap<Object, Object> result = MutableMap.of();
+        RegisteredType item = mgmt.getTypeRegistry().get(catalogId);
+        if (item==null) return result;
+        
+        result.addIfNotNull("iconUrl", item.getIconUrl());
+        return result;
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    private <T extends Entity> org.apache.brooklyn.api.entity.EntitySpec<?> toCoreEntitySpec(Class<T> clazz, String name, Map<String,String> configO, String catalogItemId) {
+        Map<String, String> config = (configO == null) ? Maps.<String,String>newLinkedHashMap() : Maps.newLinkedHashMap(configO);
+        
+        org.apache.brooklyn.api.entity.EntitySpec<? extends Entity> result;
+        if (clazz.isInterface()) {
+            result = org.apache.brooklyn.api.entity.EntitySpec.create(clazz);
+        } else {
+            // If this is a concrete class, particularly for an Application class, we want the proxy
+            // to expose all interfaces it implements.
+            Class interfaceclazz = (Application.class.isAssignableFrom(clazz)) ? Application.class : Entity.class;
+            Set<Class<?>> additionalInterfaceClazzes = Reflections.getInterfacesIncludingClassAncestors(clazz);
+            result = org.apache.brooklyn.api.entity.EntitySpec.create(interfaceclazz).impl(clazz).additionalInterfaces(additionalInterfaceClazzes);
+        }
+        
+        result.catalogItemId(catalogItemId);
+        if (!Strings.isEmpty(name)) result.displayName(name);
+        result.configure( convertFlagsToKeys(result.getImplementation(), config) );
+        return result;
+    }
+
+    private Map<?,?> convertFlagsToKeys(Class<? extends Entity> javaType, Map<?, ?> config) {
+        if (config==null || config.isEmpty() || javaType==null) return config;
+        
+        Map<String, ConfigKey<?>> configKeys = BrooklynTypes.getDefinedConfigKeys(javaType);
+        Map<Object,Object> result = new LinkedHashMap<Object,Object>();
+        for (Map.Entry<?,?> entry: config.entrySet()) {
+            log.debug("Setting key {} to {} for REST creation of {}", new Object[] { entry.getKey(), entry.getValue(), javaType});
+            Object key = configKeys.get(entry.getKey());
+            if (key==null) {
+                log.warn("Unrecognised config key {} passed to {}; will be treated as flag (and likely ignored)", entry.getKey(), javaType);
+                key = entry.getKey();
+            }
+            result.put(key, entry.getValue());
+        }
+        return result;
+    }
+    
+    public Task<?> destroy(final Application application) {
+        return mgmt.getExecutionManager().submit(
+                MutableMap.of("displayName", "destroying "+application,
+                        "description", "REST call to destroy application "+application.getDisplayName()+" ("+application+")"),
+                new Runnable() {
+            @Override
+            public void run() {
+                ((EntityInternal)application).destroy();
+                mgmt.getEntityManager().unmanage(application);
+            }
+        });
+    }
+    
+    public Task<?> expunge(final Entity entity, final boolean release) {
+        if (mgmt.getEntitlementManager().isEntitled(Entitlements.getEntitlementContext(),
+                Entitlements.INVOKE_EFFECTOR, Entitlements.EntityAndItem.of(entity, 
+                    StringAndArgument.of("expunge", MutableMap.of("release", release))))) {
+            Map<String, Object> flags = MutableMap.<String, Object>of("displayName", "expunging " + entity, "description", "REST call to expunge entity "
+                    + entity.getDisplayName() + " (" + entity + ")");
+            if (Entitlements.getEntitlementContext() != null) {
+                flags.put("tags", MutableSet.of(BrooklynTaskTags.tagForEntitlement(Entitlements.getEntitlementContext())));
+            }
+            return mgmt.getExecutionManager().submit(
+                    flags, new Runnable() {
+                        @Override
+                        public void run() {
+                            if (release)
+                                Entities.destroyCatching(entity);
+                            else
+                                mgmt.getEntityManager().unmanage(entity);
+                        }
+                    });
+        }
+        throw WebResourceUtils.forbidden("User '%s' is not authorized to expunge entity %s",
+                    Entitlements.getEntitlementContext().user(), entity);
+    }
+
+    @Deprecated
+    public static String fixLocation(String locationId) {
+        if (locationId.startsWith("/locations/") || locationId.startsWith("/v1/locations/")) {
+            log.warn("REST API using legacy URI syntax for location: "+locationId);
+            locationId = Strings.removeFromStart(locationId, "/v1/locations/");
+            locationId = Strings.removeFromStart(locationId, "/locations/");
+        }
+        return locationId;
+    }
+
+    public Object getObjectValueForDisplay(Object value) {
+        if (value==null) return null;
+        // currently everything converted to string, expanded if it is a "done" future
+        if (value instanceof Future) {
+            if (((Future<?>)value).isDone()) {
+                try {
+                    value = ((Future<?>)value).get();
+                } catch (Exception e) {
+                    value = ""+value+" (error evaluating: "+e+")";
+                }
+            }
+        }
+        
+        if (TypeCoercions.isPrimitiveOrBoxer(value.getClass())) return value;
+        return value.toString();
+    }
+
+    // currently everything converted to string, expanded if it is a "done" future
+    public String getStringValueForDisplay(Object value) {
+        if (value==null) return null;
+        return ""+getObjectValueForDisplay(value);
+    }
+
+    /** true if the URL points to content which must be resolved on the server-side (i.e. classpath)
+     *  and which is safe to do so (currently just images, though in future perhaps also javascript and html plugins)
+     *  <p>
+     *  note we do not let caller access classpath through this mechanism, 
+     *  just those which are supplied by the platform administrator e.g. as an icon url */
+    public boolean isUrlServerSideAndSafe(String url) {
+        if (Strings.isEmpty(url)) return false;
+        String ext = Files.getFileExtension(url);
+        if (Strings.isEmpty(ext)) return false;
+        MediaType mime = WebResourceUtils.getImageMediaTypeFromExtension(ext);
+        if (mime==null) return false;
+        
+        return !Urls.isUrlWithProtocol(url) || url.startsWith("classpath:");
+    }
+
+    
+    public Iterable<Entity> descendantsOfAnyType(String application, String entity) {
+        List<Entity> result = Lists.newArrayList();
+        Entity e = getEntity(application, entity);
+        gatherAllDescendants(e, result);
+        return result;
+    }
+    
+    private static void gatherAllDescendants(Entity e, List<Entity> result) {
+        if (result.add(e)) {
+            for (Entity ee: e.getChildren())
+                gatherAllDescendants(ee, result);
+        }
+    }
+
+    public Iterable<Entity> descendantsOfType(String application, String entity, final String typeRegex) {
+        Iterable<Entity> result = descendantsOfAnyType(application, entity);
+        return Iterables.filter(result, new Predicate<Entity>() {
+            @Override
+            public boolean apply(Entity entity) {
+                if (entity==null) return false;
+                return (entity.getEntityType().getName().matches(typeRegex));
+            }
+        });
+    }
+
+    public void reloadBrooklynProperties() {
+        mgmt.reloadBrooklynProperties();
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java
new file mode 100644
index 0000000..1926d5e
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java
@@ -0,0 +1,111 @@
+/*
+ * 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.util;
+
+import java.util.Set;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.rest.domain.ApiError;
+import org.apache.brooklyn.rest.domain.ApiError.Builder;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.flags.ClassCoercionException;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.exceptions.UserFacingException;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.error.YAMLException;
+
+@Provider
+public class DefaultExceptionMapper implements ExceptionMapper<Throwable> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultExceptionMapper.class);
+
+    static Set<Class<?>> warnedUnknownExceptions = MutableSet.of();
+    
+    /**
+     * Maps a throwable to a response.
+     * <p/>
+     * Returns {@link WebApplicationException#getResponse} if the exception is an instance of
+     * {@link WebApplicationException}. Otherwise maps known exceptions to responses. If no
+     * mapping is found a {@link Status#INTERNAL_SERVER_ERROR} is assumed.
+     */
+    @Override
+    public Response toResponse(Throwable throwable1) {
+        // EofException is thrown when the connection is reset,
+        // for example when refreshing the browser window.
+        // Don't depend on jetty, could be running in other environments as well.
+        if (throwable1.getClass().getName().equals("org.eclipse.jetty.io.EofException")) {
+            if (LOG.isTraceEnabled()) {
+                LOG.trace("REST request running as {} threw: {}", Entitlements.getEntitlementContext(), 
+                        Exceptions.collapse(throwable1));
+            }
+            return null;
+        }
+
+        LOG.debug("REST request running as {} threw: {}", Entitlements.getEntitlementContext(), 
+            Exceptions.collapse(throwable1));
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("Full details of "+Entitlements.getEntitlementContext()+" "+throwable1, throwable1);
+        }
+
+        Throwable throwable2 = Exceptions.getFirstInteresting(throwable1);
+        // Some methods will throw this, which gets converted automatically
+        if (throwable2 instanceof WebApplicationException) {
+            WebApplicationException wae = (WebApplicationException) throwable2;
+            return wae.getResponse();
+        }
+
+        // The nicest way for methods to provide errors, wrap as this, and the stack trace will be suppressed
+        if (throwable2 instanceof UserFacingException) {
+            return ApiError.of(throwable2.getMessage()).asBadRequestResponseJson();
+        }
+
+        // For everything else, a trace is supplied
+        
+        // Assume ClassCoercionExceptions are caused by TypeCoercions from input parameters gone wrong
+        // And IllegalArgumentException for malformed input parameters.
+        if (throwable2 instanceof ClassCoercionException || throwable2 instanceof IllegalArgumentException) {
+            return ApiError.of(throwable2).asBadRequestResponseJson();
+        }
+
+        // YAML exception 
+        if (throwable2 instanceof YAMLException) {
+            return ApiError.builder().message(throwable2.getMessage()).prefixMessage("Invalid YAML").build().asBadRequestResponseJson();
+        }
+
+        if (!Exceptions.isPrefixBoring(throwable2)) {
+            if ( warnedUnknownExceptions.add( throwable2.getClass() )) {
+                LOG.warn("REST call generated exception type "+throwable2.getClass()+" unrecognized in "+getClass()+" (subsequent occurrences will be logged debug only): " + throwable2, throwable2);
+            }
+        }
+        
+        Builder rb = ApiError.builderFromThrowable(Exceptions.collapse(throwable2));
+        if (Strings.isBlank(rb.getMessage()))
+            rb.message("Internal error. Contact server administrator to consult logs for more details.");
+        return rb.build().asResponse(Status.INTERNAL_SERVER_ERROR, MediaType.APPLICATION_JSON_TYPE);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java
new file mode 100644
index 0000000..32bb66d
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.location.LocationConfigKeys;
+
+public class EntityLocationUtils {
+
+    protected final ManagementContext context;
+
+    public EntityLocationUtils(ManagementContext ctx) {
+        this.context = ctx;
+    }
+    
+    /* Returns the number of entites at each location for which the geographic coordinates are known. */
+    public Map<Location, Integer> countLeafEntitiesByLocatedLocations() {
+        Map<Location, Integer> result = new LinkedHashMap<Location, Integer>();
+        for (Entity e: context.getApplications()) {
+            countLeafEntitiesByLocatedLocations(e, null, result);
+        }
+        return result;
+    }
+
+    protected void countLeafEntitiesByLocatedLocations(Entity target, Entity locatedParent, Map<Location, Integer> result) {
+        if (isLocatedLocation(target))
+            locatedParent = target;
+        if (!target.getChildren().isEmpty()) {
+            // non-leaf - inspect children
+            for (Entity child: target.getChildren()) 
+                countLeafEntitiesByLocatedLocations(child, locatedParent, result);
+        } else {
+            // leaf node - increment location count
+            if (locatedParent!=null) {
+                for (Location l: locatedParent.getLocations()) {
+                    Location ll = getMostGeneralLocatedLocation(l);
+                    if (ll!=null) {
+                        Integer count = result.get(ll);
+                        if (count==null) count = 1;
+                        else count++;
+                        result.put(ll, count);
+                    }
+                }
+            }
+        }
+    }
+
+    protected Location getMostGeneralLocatedLocation(Location l) {
+        if (l==null) return null;
+        if (!isLocatedLocation(l)) return null;
+        Location ll = getMostGeneralLocatedLocation(l.getParent());
+        if (ll!=null) return ll;
+        return l;
+    }
+
+    protected boolean isLocatedLocation(Entity target) {
+        for (Location l: target.getLocations())
+            if (isLocatedLocation(l)) return true;
+        return false;
+    }
+    protected boolean isLocatedLocation(Location l) {
+        return l.getConfig(LocationConfigKeys.LATITUDE)!=null && l.getConfig(LocationConfigKeys.LONGITUDE)!=null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java
new file mode 100644
index 0000000..106b73a
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java
@@ -0,0 +1,86 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.Provider;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import javax.ws.rs.core.Context;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.provider.FormEncodingProvider;
+
+/**
+ * A MessageBodyReader producing a <code>Map&lt;String, Object&gt;</code>, where Object
+ * is either a <code>String</code>, a <code>List&lt;String&gt;</code> or null.
+ */
+@Provider
+@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+public class FormMapProvider implements MessageBodyReader<Map<String, Object>> {
+
+    @Context
+    private MessageContext mc;
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        if (!Map.class.equals(type) || !(genericType instanceof ParameterizedType)) {
+            return false;
+        }
+        ParameterizedType parameterized = (ParameterizedType) genericType;
+        return parameterized.getActualTypeArguments().length == 2 &&
+                parameterized.getActualTypeArguments()[0] == String.class &&
+                parameterized.getActualTypeArguments()[1] == Object.class;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Override
+    public Map<String, Object> readFrom(Class<Map<String, Object>> type, Type genericType, Annotation[] annotations,
+            MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+            throws IOException, WebApplicationException {
+        FormEncodingProvider delegate = new FormEncodingProvider();
+        MultivaluedMap<String, String> multi = (MultivaluedMap<String, String>) delegate.readFrom(MultivaluedMap.class, null, null,
+                        mediaType, httpHeaders, entityStream);
+
+        Map<String, Object> map = Maps.newHashMapWithExpectedSize(multi.keySet().size());
+        for (String key : multi.keySet()) {
+            List<String> value = multi.get(key);
+            if (value.size() > 1) {
+                map.put(key, Lists.newArrayList(value));
+            } else if (value.size() == 1) {
+                map.put(key, Iterables.getOnlyElement(value));
+            } else {
+                map.put(key, null);
+            }
+        }
+        return map;
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java
new file mode 100644
index 0000000..ae90d0e
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util;
+
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+
+@Provider
+// Needed by tests in rest-resources module and by main code in rest-server
+public class ManagementContextProvider implements ContextResolver<ManagementContext> {
+
+    private ManagementContext mgmt;
+
+    public ManagementContextProvider(ManagementContext mgmt) {
+        this.mgmt = mgmt;
+    }
+
+    @Override
+    public ManagementContext getContext(Class<?> type) {
+        return mgmt;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/OsgiCompat.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/OsgiCompat.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/OsgiCompat.java
new file mode 100644
index 0000000..6669f95
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/OsgiCompat.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2015 The Apache Software Foundation.
+ *
+ * Licensed 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.util;
+
+import javax.servlet.ServletContext;
+
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.core.server.BrooklynServiceAttributes;
+import org.apache.brooklyn.util.core.osgi.Compat;
+
+/**
+ * Compatibility methods between karaf launcher and monolithic launcher.
+ *
+ * @todo Remove after transition to karaf launcher.
+ */
+public class OsgiCompat {
+
+    public static ManagementContext getManagementContext(ServletContext servletContext) {
+        ManagementContext managementContext = Compat.getInstance().getManagementContext();
+        if (managementContext == null && servletContext != null) {
+            managementContext = (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT);
+        }
+        return managementContext;
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java
new file mode 100644
index 0000000..e573bf6
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java
@@ -0,0 +1,23 @@
+/*
+ * 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.util;
+
+public interface ShutdownHandler {
+    void onShutdownRequest();
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java
new file mode 100644
index 0000000..bae2922
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util;
+
+import javax.annotation.Nullable;
+import javax.ws.rs.ext.ContextResolver;
+import javax.ws.rs.ext.Provider;
+
+
+@Provider
+public class ShutdownHandlerProvider implements ContextResolver<ShutdownHandler> {
+
+    private ShutdownHandler shutdownHandler;
+
+    public ShutdownHandlerProvider(@Nullable ShutdownHandler instance) {
+        this.shutdownHandler = instance;
+    }
+
+    @Override
+    public ShutdownHandler getContext(Class<?> type) {
+        return shutdownHandler;
+    }
+
+}
+

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java
new file mode 100644
index 0000000..8c25fda
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java
@@ -0,0 +1,27 @@
+/*
+ * 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.util;
+
+
+/**
+ * @deprecated since 0.7.0 use {@link org.apache.brooklyn.util.net.URLParamEncoder}
+ */
+public class URLParamEncoder extends org.apache.brooklyn.util.net.URLParamEncoder {
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java
new file mode 100644
index 0000000..5894700
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java
@@ -0,0 +1,197 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
+import org.apache.brooklyn.rest.domain.ApiError;
+import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import javax.ws.rs.core.UriBuilder;
+
+public class WebResourceUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(WebResourceUtils.class);
+
+    /** @throws WebApplicationException with an ApiError as its body and the given status as its response code. */
+    public static WebApplicationException throwWebApplicationException(Response.Status status, String format, Object... args) {
+        String msg = String.format(format, args);
+        if (log.isDebugEnabled()) {
+            log.debug("responding {} {} ({})",
+                    new Object[]{status.getStatusCode(), status.getReasonPhrase(), msg});
+        }
+        ApiError apiError = ApiError.builder().message(msg).errorCode(status).build();
+        // including a Throwable is the only way to include a message with the WebApplicationException - ugly!
+        throw new WebApplicationException(new Throwable(apiError.toString()), apiError.asJsonResponse());
+    }
+
+    /** @throws WebApplicationException With code 500 internal server error */
+    public static WebApplicationException serverError(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.INTERNAL_SERVER_ERROR, format, args);
+    }
+
+    /** @throws WebApplicationException With code 400 bad request */
+    public static WebApplicationException badRequest(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.BAD_REQUEST, format, args);
+    }
+
+    /** @throws WebApplicationException With code 401 unauthorized */
+    public static WebApplicationException unauthorized(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.UNAUTHORIZED, format, args);
+    }
+
+    /** @throws WebApplicationException With code 403 forbidden */
+    public static WebApplicationException forbidden(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.FORBIDDEN, format, args);
+    }
+
+    /** @throws WebApplicationException With code 404 not found */
+    public static WebApplicationException notFound(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.NOT_FOUND, format, args);
+    }
+
+    /** @throws WebApplicationException With code 412 precondition failed */
+    public static WebApplicationException preconditionFailed(String format, Object... args) {
+        return throwWebApplicationException(Response.Status.PRECONDITION_FAILED, format, args);
+    }
+
+    public final static Map<String,com.google.common.net.MediaType> IMAGE_FORMAT_MIME_TYPES = ImmutableMap.<String, com.google.common.net.MediaType>builder()
+            .put("jpg", com.google.common.net.MediaType.JPEG)
+            .put("jpeg", com.google.common.net.MediaType.JPEG)
+            .put("png", com.google.common.net.MediaType.PNG)
+            .put("gif", com.google.common.net.MediaType.GIF)
+            .put("svg", com.google.common.net.MediaType.SVG_UTF_8)
+            .build();
+    
+    public static MediaType getImageMediaTypeFromExtension(String extension) {
+        com.google.common.net.MediaType mime = IMAGE_FORMAT_MIME_TYPES.get(extension.toLowerCase());
+        if (mime==null) return null;
+        try {
+            return MediaType.valueOf(mime.toString());
+        } catch (Exception e) {
+            log.warn("Unparseable MIME type "+mime+"; ignoring ("+e+")");
+            Exceptions.propagateIfFatal(e);
+            return null;
+        }
+    }
+
+    /** as {@link #getValueForDisplay(ObjectMapper, Object, boolean, boolean)} with no mapper
+     * (so will only handle a subset of types) */
+    public static Object getValueForDisplay(Object value, boolean preferJson, boolean isJerseyReturnValue) {
+        return getValueForDisplay(null, value, preferJson, isJerseyReturnValue);
+    }
+    
+    /** returns an object which jersey will handle nicely, converting to json,
+     * sometimes wrapping in quotes if needed (for outermost json return types);
+     * if json is not preferred, this simply applies a toString-style rendering */ 
+    public static Object getValueForDisplay(ObjectMapper mapper, Object value, boolean preferJson, boolean isJerseyReturnValue) {
+        if (preferJson) {
+            if (value==null) return null;
+            Object result = value;
+            // no serialization checks required, with new smart-mapper which does toString
+            // (note there is more sophisticated logic in git history however)
+            result = value;
+            
+            if (isJerseyReturnValue) {
+                if (result instanceof String) {
+                    // Jersey does not do json encoding if the return type is a string,
+                    // expecting the returner to do the json encoding himself
+                    // cf discussion at https://github.com/dropwizard/dropwizard/issues/231
+                    result = JavaStringEscapes.wrapJavaString((String)result);
+                }
+            }
+            
+            return result;
+        } else {
+            if (value==null) return "";
+            return value.toString();            
+        }
+    }
+
+    public static String getPathFromVersionedId(String versionedId) {
+        if (CatalogUtils.looksLikeVersionedId(versionedId)) {
+            String symbolicName = CatalogUtils.getSymbolicNameFromVersionedId(versionedId);
+            String version = CatalogUtils.getVersionFromVersionedId(versionedId);
+            return Urls.encode(symbolicName) + "/" + Urls.encode(version);
+        } else {
+            return Urls.encode(versionedId);
+        }
+    }
+
+    /** Sets the {@link HttpServletResponse} target (last argument) from the given source {@link Response};
+     * useful in filters where we might have a {@link Response} and need to set up an {@link HttpServletResponse}.
+     */
+    public static void applyJsonResponse(ServletContext servletContext, Response source, HttpServletResponse target) throws IOException {
+        target.setStatus(source.getStatus());
+        target.setContentType(MediaType.APPLICATION_JSON);
+        target.setCharacterEncoding("UTF-8");
+        target.getWriter().write(BrooklynJacksonJsonProvider.findAnyObjectMapper(servletContext, null).writeValueAsString(source.getEntity()));
+    }
+
+    /**
+     * Provides a builder with the REST URI of a resource.
+     * @param baseUriBuilder An {@link UriBuilder} pointing at the base of the REST API.
+     * @param resourceClass The target resource class.
+     * @return A new {@link UriBuilder} that targets the specified REST resource.
+     */
+    public static UriBuilder resourceUriBuilder(UriBuilder baseUriBuilder, Class<?> resourceClass) {
+        return UriBuilder.fromPath(baseUriBuilder.build().getPath())
+                .path(resourceClass);
+    }
+
+    /**
+     * Provides a builder with the REST URI of a service provided by a resource.
+     * @param baseUriBuilder An {@link UriBuilder} pointing at the base of the REST API.
+     * @param resourceClass The target resource class.
+     * @param method The target service (e.g. class method).
+     * @return A new {@link UriBuilder} that targets the specified service of the REST resource.
+     */
+    public static UriBuilder serviceUriBuilder(UriBuilder baseUriBuilder, Class<?> resourceClass, String method) {
+        return resourceUriBuilder(baseUriBuilder, resourceClass).path(resourceClass, method);
+    }
+
+    /**
+     * Provides a builder with the absolute REST URI of a service provided by a resource.
+     * @param baseUriBuilder An {@link UriBuilder} pointing at the base of the REST API.
+     * @param resourceClass The target resource class.
+     * @param method The target service (e.g. class method).
+     * @return A new {@link UriBuilder} that targets the specified service of the REST resource.
+     */
+    public static UriBuilder serviceAbsoluteUriBuilder(UriBuilder baseUriBuilder, Class<?> resourceClass, String method) {
+        return  baseUriBuilder
+                    .path(resourceClass)
+                    .path(resourceClass, method);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java
new file mode 100644
index 0000000..93cae3f
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java
@@ -0,0 +1,173 @@
+/*
+ * 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.util.json;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.api.objs.BrooklynObject;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+public class BidiSerialization {
+
+    protected final static ThreadLocal<Boolean> STRICT_SERIALIZATION = new ThreadLocal<Boolean>(); 
+
+    /**
+     * Sets strict serialization on, or off (the default), for the current thread.
+     * Recommended to be used in a <code>try { ... } finally { ... }</code> block
+     * with {@link #clearStrictSerialization()} at the end.
+     * <p>
+     * With strict serialization, classes must have public fields or annotated fields, else they will not be serialized.
+     */
+    public static void setStrictSerialization(Boolean value) {
+        STRICT_SERIALIZATION.set(value);
+    }
+
+    public static void clearStrictSerialization() {
+        STRICT_SERIALIZATION.remove();
+    }
+
+    public static boolean isStrictSerialization() {
+        Boolean result = STRICT_SERIALIZATION.get();
+        if (result!=null) return result;
+        return false;
+    }
+
+
+    public abstract static class AbstractWithManagementContextSerialization<T> {
+
+        protected class Serializer extends JsonSerializer<T> {
+            @Override
+            public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+                AbstractWithManagementContextSerialization.this.serialize(value, jgen, provider);
+            }
+        }
+        
+        protected class Deserializer extends JsonDeserializer<T> {
+            @Override
+            public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+                return AbstractWithManagementContextSerialization.this.deserialize(jp, ctxt);
+            }
+        }
+        
+        protected final Serializer serializer = new Serializer();
+        protected final Deserializer deserializer = new Deserializer();
+        protected final Class<T> type;
+        protected final ManagementContext mgmt;
+        
+        public AbstractWithManagementContextSerialization(Class<T> type, ManagementContext mgmt) {
+            this.type = type;
+            this.mgmt = mgmt;
+        }
+        
+        public JsonSerializer<T> getSerializer() {
+            return serializer;
+        }
+        
+        public JsonDeserializer<T> getDeserializer() {
+            return deserializer;
+        }
+
+        public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+            jgen.writeStartObject();
+            writeBody(value, jgen, provider);
+            jgen.writeEndObject();
+        }
+
+        protected void writeBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+            jgen.writeStringField("type", value.getClass().getCanonicalName());
+            customWriteBody(value, jgen, provider);
+        }
+
+        public abstract void customWriteBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException;
+
+        public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+            @SuppressWarnings("unchecked")
+            Map<Object,Object> values = jp.readValueAs(Map.class);
+            String type = (String) values.get("type");
+            return customReadBody(type, values, jp, ctxt);
+        }
+
+        protected abstract T customReadBody(String type, Map<Object, Object> values, JsonParser jp, DeserializationContext ctxt) throws IOException;
+
+        public void install(SimpleModule module) {
+            module.addSerializer(type, serializer);
+            module.addDeserializer(type, deserializer);
+        }
+    }
+    
+    public static class ManagementContextSerialization extends AbstractWithManagementContextSerialization<ManagementContext> {
+        public ManagementContextSerialization(ManagementContext mgmt) { super(ManagementContext.class, mgmt); }
+        @Override
+        public void customWriteBody(ManagementContext value, JsonGenerator jgen, SerializerProvider provider) throws IOException {}
+        @Override
+        protected ManagementContext customReadBody(String type, Map<Object, Object> values, JsonParser jp, DeserializationContext ctxt) throws IOException {
+            return mgmt;
+        }
+    }
+    
+    public abstract static class AbstractBrooklynObjectSerialization<T extends BrooklynObject> extends AbstractWithManagementContextSerialization<T> {
+        public AbstractBrooklynObjectSerialization(Class<T> type, ManagementContext mgmt) { 
+            super(type, mgmt);
+        }
+        @Override
+        protected void writeBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+            jgen.writeStringField("type", type.getCanonicalName());
+            customWriteBody(value, jgen, provider);
+        }
+        @Override
+        public void customWriteBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+            jgen.writeStringField("id", value.getId());
+        }
+        @Override
+        protected T customReadBody(String type, Map<Object, Object> values, JsonParser jp, DeserializationContext ctxt) throws IOException {
+            return getInstanceFromId((String) values.get("id"));
+        }
+        protected abstract T getInstanceFromId(String id);
+    }
+
+    public static class EntitySerialization extends AbstractBrooklynObjectSerialization<Entity> {
+        public EntitySerialization(ManagementContext mgmt) { super(Entity.class, mgmt); }
+        @Override protected Entity getInstanceFromId(String id) { return mgmt.getEntityManager().getEntity(id); }
+    }
+    public static class LocationSerialization extends AbstractBrooklynObjectSerialization<Location> {
+        public LocationSerialization(ManagementContext mgmt) { super(Location.class, mgmt); }
+        @Override protected Location getInstanceFromId(String id) { return mgmt.getLocationManager().getLocation(id); }
+    }
+    // TODO how to look up policies and enrichers? (not essential...)
+//    public static class PolicySerialization extends AbstractBrooklynObjectSerialization<Policy> {
+//        public EntitySerialization(ManagementContext mgmt) { super(Policy.class, mgmt); }
+//        @Override protected Policy getKind(String id) { return mgmt.getEntityManager().getEntity(id); }
+//    }
+//    public static class EnricherSerialization extends AbstractBrooklynObjectSerialization<Enricher> {
+//        public EntitySerialization(ManagementContext mgmt) { super(Entity.class, mgmt); }
+//        @Override protected Enricher getKind(String id) { return mgmt.getEntityManager().getEntity(id); }
+//    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java
new file mode 100644
index 0000000..5568208
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java
@@ -0,0 +1,177 @@
+/*
+ * 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.util.json;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.core.server.BrooklynServiceAttributes;
+import org.apache.brooklyn.rest.util.OsgiCompat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
+
+public class BrooklynJacksonJsonProvider extends JacksonJsonProvider implements
+        //CXF only looks at the interfaces of this class to determine if the Provider is a MessageBodyWriter/Reader
+        MessageBodyWriter<Object>, MessageBodyReader<Object> {
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynJacksonJsonProvider.class);
+
+    public static final String BROOKLYN_REST_OBJECT_MAPPER = BrooklynServiceAttributes.BROOKLYN_REST_OBJECT_MAPPER;
+
+    @Context protected ServletContext servletContext;
+
+    protected ObjectMapper ourMapper;
+    protected boolean notFound = false;
+
+    private ManagementContext mgmt;
+
+    @Override
+    public ObjectMapper locateMapper(Class<?> type, MediaType mediaType) {
+        if (ourMapper != null)
+            return ourMapper;
+
+        findSharedMapper();
+
+        if (ourMapper != null)
+            return ourMapper;
+
+        if (!notFound) {
+            log.warn("Management context not available; using default ObjectMapper in "+this);
+            notFound = true;
+        }
+
+        return super.locateMapper(Object.class, MediaType.APPLICATION_JSON_TYPE);
+    }
+
+    protected synchronized ObjectMapper findSharedMapper() {
+        if (ourMapper != null || notFound)
+            return ourMapper;
+
+        ourMapper = findSharedObjectMapper(servletContext, mgmt);
+        if (ourMapper == null) return null;
+
+        if (notFound) {
+            notFound = false;
+        }
+        log.debug("Found mapper "+ourMapper+" for "+this+", creating custom Brooklyn mapper");
+
+        return ourMapper;
+    }
+
+    /**
+     * Finds a shared {@link ObjectMapper} or makes a new one, stored against the servlet context;
+     * returns null if a shared instance cannot be created.
+     */
+    public static ObjectMapper findSharedObjectMapper(ServletContext servletContext, ManagementContext mgmt) {
+        checkNotNull(mgmt, "mgmt");
+        if (servletContext != null) {
+            synchronized (servletContext) {
+                boolean isServletContextNull = false;
+                try {
+                    ObjectMapper mapper = (ObjectMapper) servletContext.getAttribute(BROOKLYN_REST_OBJECT_MAPPER);
+                    if (mapper != null) return mapper;
+                } catch (NullPointerException e) {
+                    // CXF always injects a ThreadLocalServletContext that may return null later on.
+                    // Ignore this case so this provider can be used outside the REST server, such as the CXF client during tests.
+                    isServletContextNull = true;
+                }
+
+                if (!isServletContextNull) {
+                    ObjectMapper mapper = newPrivateObjectMapper(mgmt);
+                    servletContext.setAttribute(BROOKLYN_REST_OBJECT_MAPPER, mapper);
+                    return mapper;
+                }
+            }
+        }
+        if (mgmt != null) {
+            synchronized (mgmt) {
+                ConfigKey<ObjectMapper> key = ConfigKeys.newConfigKey(ObjectMapper.class, BROOKLYN_REST_OBJECT_MAPPER);
+                ObjectMapper mapper = mgmt.getConfig().getConfig(key);
+                if (mapper != null) return mapper;
+
+                mapper = newPrivateObjectMapper(mgmt);
+                log.debug("Storing new ObjectMapper against "+mgmt+" because no ServletContext available: "+mapper);
+                ((BrooklynProperties)mgmt.getConfig()).put(key, mapper);
+                return mapper;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Like {@link #findSharedObjectMapper(ServletContext, ManagementContext)} but will create a private
+     * ObjectMapper if it can, from the servlet context and/or the management context, or else fail
+     */
+    public static ObjectMapper findAnyObjectMapper(ServletContext servletContext, ManagementContext mgmt) {
+        ObjectMapper mapper = findSharedObjectMapper(servletContext, mgmt);
+        if (mapper != null) return mapper;
+
+        if (mgmt == null && servletContext != null) {
+            mgmt = getManagementContext(servletContext);
+        }
+        return newPrivateObjectMapper(mgmt);
+    }
+
+    /**
+     * @return A new Brooklyn-specific ObjectMapper.
+     *   Normally {@link #findSharedObjectMapper(ServletContext, ManagementContext)} is preferred
+     */
+    public static ObjectMapper newPrivateObjectMapper(ManagementContext mgmt) {
+        if (mgmt == null) {
+            throw new IllegalStateException("No management context available for creating ObjectMapper");
+        }
+
+        ConfigurableSerializerProvider sp = new ConfigurableSerializerProvider();
+        sp.setUnknownTypeSerializer(new ErrorAndToStringUnknownTypeSerializer());
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setSerializerProvider(sp);
+        mapper.setVisibilityChecker(new PossiblyStrictPreferringFieldsVisibilityChecker());
+
+        SimpleModule mapperModule = new SimpleModule("Brooklyn", new Version(0, 0, 0, "ignored"));
+
+        new BidiSerialization.ManagementContextSerialization(mgmt).install(mapperModule);
+        new BidiSerialization.EntitySerialization(mgmt).install(mapperModule);
+        new BidiSerialization.LocationSerialization(mgmt).install(mapperModule);
+
+        mapperModule.addSerializer(new MultimapSerializer());
+        mapper.registerModule(mapperModule);
+
+        return mapper;
+    }
+
+    public static ManagementContext getManagementContext(ServletContext servletContext) {
+        return OsgiCompat.getManagementContext(servletContext);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java
new file mode 100644
index 0000000..1b87e76
--- /dev/null
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util.json;
+
+import java.io.IOException;
+
+import org.apache.brooklyn.util.exceptions.Exceptions;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonStreamContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
+import com.fasterxml.jackson.databind.ser.SerializerFactory;
+
+/** allows the serializer-of-last-resort to be customized, ie used for unknown-types */
+final class ConfigurableSerializerProvider extends DefaultSerializerProvider {
+
+    protected JsonSerializer<Object> unknownTypeSerializer;
+
+    public ConfigurableSerializerProvider() {}
+
+    @Override
+    public DefaultSerializerProvider createInstance(SerializationConfig config, SerializerFactory jsf) {
+        return new ConfigurableSerializerProvider(config, this, jsf);
+    }
+
+    public ConfigurableSerializerProvider(SerializationConfig config, ConfigurableSerializerProvider src, SerializerFactory jsf) {
+        super(src, config, jsf);
+        unknownTypeSerializer = src.unknownTypeSerializer;
+    }
+
+    @Override
+    public JsonSerializer<Object> getUnknownTypeSerializer(Class<?> unknownType) {
+        if (unknownTypeSerializer!=null) return unknownTypeSerializer;
+        return super.getUnknownTypeSerializer(unknownType);
+    }
+
+    public void setUnknownTypeSerializer(JsonSerializer<Object> unknownTypeSerializer) {
+        this.unknownTypeSerializer = unknownTypeSerializer;
+    }
+
+    @Override
+    public void serializeValue(JsonGenerator jgen, Object value) throws IOException {
+        JsonStreamContext ctxt = jgen.getOutputContext();
+        try {
+            super.serializeValue(jgen, value);
+        } catch (Exception e) {
+            onSerializationException(ctxt, jgen, value, e);
+        }
+    }
+
+    @Override
+    public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType) throws IOException {
+        JsonStreamContext ctxt = jgen.getOutputContext();
+        try {
+            super.serializeValue(jgen, value, rootType);
+        } catch (Exception e) {
+            onSerializationException(ctxt, jgen, value, e);
+        }
+    }
+
+    protected void onSerializationException(JsonStreamContext ctxt, JsonGenerator jgen, Object value, Exception e) throws IOException {
+        Exceptions.propagateIfFatal(e);
+
+        JsonSerializer<Object> unknownTypeSerializer = getUnknownTypeSerializer(value.getClass());
+        if (unknownTypeSerializer instanceof ErrorAndToStringUnknownTypeSerializer) {
+            ((ErrorAndToStringUnknownTypeSerializer)unknownTypeSerializer).serializeFromError(ctxt, e, value, jgen, this);
+        } else {
+            unknownTypeSerializer.serialize(value, jgen, this);
+        }
+    }
+}