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<String, Object></code>, where Object
+ * is either a <code>String</code>, a <code>List<String></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);
+ }
+ }
+}