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); + } + } +}