http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java new file mode 100644 index 0000000..25b3f39 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/DefaultExceptionMapper.java @@ -0,0 +1,102 @@ +/* + * 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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.error.YAMLException; + +import brooklyn.management.entitlement.Entitlements; +import brooklyn.rest.domain.ApiError; +import brooklyn.rest.domain.ApiError.Builder; +import brooklyn.util.collections.MutableSet; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.exceptions.UserFacingException; +import brooklyn.util.flags.ClassCoercionException; +import brooklyn.util.text.Strings; + +@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) { + + 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/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/EntityLocationUtils.java new file mode 100644 index 0000000..d1c02e8 --- /dev/null +++ b/usage/rest-server/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 brooklyn.entity.Entity; +import brooklyn.location.Location; +import brooklyn.location.basic.LocationConfigKeys; +import brooklyn.management.ManagementContext; + +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/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java new file mode 100644 index 0000000..2b5c19b --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/FormMapProvider.java @@ -0,0 +1,81 @@ +/* + * 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 com.sun.jersey.core.impl.provider.entity.FormMultivaluedMapProvider; +import com.sun.jersey.core.util.MultivaluedMapImpl; + +/** + * 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>> { + + @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; + } + + @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 { + FormMultivaluedMapProvider delegate = new FormMultivaluedMapProvider(); + MultivaluedMap<String, String> multi = new MultivaluedMapImpl(); + multi = delegate.readFrom(multi, mediaType, 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/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java new file mode 100644 index 0000000..4b24f05 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ManagementContextProvider.java @@ -0,0 +1,33 @@ +/* + * 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.core.Context; + +import brooklyn.management.ManagementContext; + +import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider; + +public class ManagementContextProvider extends SingletonTypeInjectableProvider<Context, ManagementContext> { + + public ManagementContextProvider(ManagementContext instance) { + super(ManagementContext.class, instance); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandler.java new file mode 100644 index 0000000..e573bf6 --- /dev/null +++ b/usage/rest-server/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/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java new file mode 100644 index 0000000..f499ca0 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/ShutdownHandlerProvider.java @@ -0,0 +1,30 @@ +/* + * 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.core.Context; + +import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider; + +public class ShutdownHandlerProvider extends SingletonTypeInjectableProvider<Context, ShutdownHandler> { + public ShutdownHandlerProvider(@Nullable ShutdownHandler instance) { + super(ShutdownHandler.class, instance); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/URLParamEncoder.java new file mode 100644 index 0000000..bfb6a3b --- /dev/null +++ b/usage/rest-server/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 brooklyn.util.net.URLParamEncoder} + */ +public class URLParamEncoder extends brooklyn.util.net.URLParamEncoder { + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java new file mode 100644 index 0000000..67508bb --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/WebResourceUtils.java @@ -0,0 +1,162 @@ +/* + * 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.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.catalog.internal.CatalogUtils; +import brooklyn.rest.domain.ApiError; +import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider; +import brooklyn.util.exceptions.Exceptions; +import brooklyn.util.net.Urls; +import brooklyn.util.text.StringEscapes.JavaStringEscapes; + +import com.google.common.collect.ImmutableMap; +import com.sun.jersey.spi.container.ContainerResponse; + +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.getIdFromVersionedId(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}. + * Similar to {@link ContainerResponse#setResponse(Response)}; nothing like that seems to be available for {@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())); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java new file mode 100644 index 0000000..434962e --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BidiSerialization.java @@ -0,0 +1,175 @@ +/* + * 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.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.JsonDeserializer; +import org.codehaus.jackson.map.JsonSerializer; +import org.codehaus.jackson.map.SerializerProvider; +import org.codehaus.jackson.map.module.SimpleModule; + +import brooklyn.basic.BrooklynObject; +import brooklyn.entity.Entity; +import brooklyn.location.Location; +import brooklyn.management.ManagementContext; + +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, JsonProcessingException { + AbstractWithManagementContextSerialization.this.serialize(value, jgen, provider); + } + } + + protected class Deserializer extends JsonDeserializer<T> { + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + 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, JsonProcessingException { + jgen.writeStartObject(); + writeBody(value, jgen, provider); + jgen.writeEndObject(); + } + + protected void writeBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException, JsonProcessingException { + jgen.writeStringField("type", value.getClass().getCanonicalName()); + customWriteBody(value, jgen, provider); + } + + public abstract void customWriteBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException; + + public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + @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, JsonProcessingException; + + 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, JsonProcessingException {} + @Override + protected ManagementContext customReadBody(String type, Map<Object, Object> values, JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + 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, JsonGenerationException, JsonProcessingException { + jgen.writeStringField("type", type.getCanonicalName()); + customWriteBody(value, jgen, provider); + } + @Override + public void customWriteBody(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeStringField("id", value.getId()); + } + @Override + protected T customReadBody(String type, Map<Object, Object> values, JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + 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/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java new file mode 100644 index 0000000..7362c6d --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonJsonProvider.java @@ -0,0 +1,172 @@ +/* + * 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 javax.servlet.ServletContext; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.codehaus.jackson.Version; +import org.codehaus.jackson.jaxrs.JacksonJsonProvider; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.module.SimpleModule; +import org.codehaus.jackson.map.type.TypeFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.config.BrooklynProperties; +import brooklyn.config.BrooklynServiceAttributes; +import brooklyn.config.ConfigKey; +import brooklyn.entity.basic.ConfigKeys; +import brooklyn.management.ManagementContext; +import brooklyn.management.ManagementContextInjectable; + +public class BrooklynJacksonJsonProvider extends JacksonJsonProvider implements ManagementContextInjectable { + + 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; + + 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) { + if (servletContext != null) { + synchronized (servletContext) { + ObjectMapper mapper = (ObjectMapper) servletContext.getAttribute(BROOKLYN_REST_OBJECT_MAPPER); + if (mapper != null) return mapper; + + mapper = newPrivateObjectMapper(getManagementContext(servletContext)); + 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"); + } + + SerializationConfig defaultConfig = new ObjectMapper().getSerializationConfig(); + SerializationConfig sc = new SerializationConfig( + defaultConfig.getClassIntrospector() /* ObjectMapper.DEFAULT_INTROSPECTOR */, + defaultConfig.getAnnotationIntrospector() /* ObjectMapper.DEFAULT_ANNOTATION_INTROSPECTOR */, + new PossiblyStrictPreferringFieldsVisibilityChecker(), + null, null, TypeFactory.defaultInstance(), null); + + ConfigurableSerializerProvider sp = new ConfigurableSerializerProvider(); + sp.setUnknownTypeSerializer(new ErrorAndToStringUnknownTypeSerializer()); + + ObjectMapper mapper = new ObjectMapper(null, sp, null, sc, null); + 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) { + if (servletContext == null) + return null; + + return (ManagementContext) servletContext.getAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT); + } + + @Override + public void injectManagementContext(ManagementContext mgmt) { + this.mgmt = mgmt; + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java new file mode 100644 index 0000000..20078df --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ConfigurableSerializerProvider.java @@ -0,0 +1,94 @@ +/* + * 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.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonStreamContext; +import org.codehaus.jackson.map.JsonSerializer; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.SerializerFactory; +import org.codehaus.jackson.map.ser.StdSerializerProvider; +import org.codehaus.jackson.type.JavaType; + +import brooklyn.util.exceptions.Exceptions; + +/** allows the serializer-of-last-resort to be customized, ie used for unknown-types */ +final class ConfigurableSerializerProvider extends StdSerializerProvider { + + public ConfigurableSerializerProvider() {} + + public ConfigurableSerializerProvider(SerializationConfig config) { + // NB: not usually necessary to pass config, as object mapper gets its own config set explicitly + this(config, new ConfigurableSerializerProvider(), null); + } + + public ConfigurableSerializerProvider(SerializationConfig config, ConfigurableSerializerProvider src, SerializerFactory jsf) { + super(config, src, jsf); + unknownTypeSerializer = src.unknownTypeSerializer; + } + + protected StdSerializerProvider createInstance(SerializationConfig config, SerializerFactory jsf) { + return new ConfigurableSerializerProvider(config, this, jsf); + } + + protected JsonSerializer<Object> unknownTypeSerializer; + + 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 + protected void _serializeValue(JsonGenerator jgen, Object value) throws IOException, JsonProcessingException { + JsonStreamContext ctxt = jgen.getOutputContext(); + try { + super._serializeValue(jgen, value); + } catch (Exception e) { + onSerializationException(ctxt, jgen, value, e); + } + } + + @Override + protected void _serializeValue(JsonGenerator jgen, Object value, JavaType rootType) throws IOException, JsonProcessingException { + 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, JsonProcessingException { + 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); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ErrorAndToStringUnknownTypeSerializer.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ErrorAndToStringUnknownTypeSerializer.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ErrorAndToStringUnknownTypeSerializer.java new file mode 100644 index 0000000..b865cb0 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/ErrorAndToStringUnknownTypeSerializer.java @@ -0,0 +1,125 @@ +/* + * 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.io.NotSerializableException; +import java.util.Collections; +import java.util.Set; + +import javax.annotation.Nullable; + +import org.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonStreamContext; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.SerializerProvider; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.codehaus.jackson.map.ser.impl.UnknownSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import brooklyn.util.collections.MutableSet; +import brooklyn.util.javalang.Reflections; + +/** + * for non-json-serializable classes (quite a lot of them!) simply provide a sensible error message and a toString. + * TODO maybe we want to attempt to serialize fields instead? (but being careful not to be self-referential!) + */ +public class ErrorAndToStringUnknownTypeSerializer extends UnknownSerializer { + + private static final Logger log = LoggerFactory.getLogger(ErrorAndToStringUnknownTypeSerializer.class); + private static Set<String> WARNED_CLASSES = Collections.synchronizedSet(MutableSet.<String>of()); + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + if (BidiSerialization.isStrictSerialization()) + throw new JsonMappingException("Cannot serialize object containing "+value.getClass().getName()+" when strict serialization requested"); + + serializeFromError(jgen.getOutputContext(), null, value, jgen, provider); + } + + public void serializeFromError(JsonStreamContext ctxt, @Nullable Exception error, Object value, JsonGenerator jgen, SerializerProvider configurableSerializerProvider) throws JsonGenerationException, IOException { + if (log.isDebugEnabled()) + log.debug("Recovering from json serialization error, serializing "+value+": "+error); + + if (BidiSerialization.isStrictSerialization()) + throw new JsonMappingException("Cannot serialize " + + (ctxt!=null && !ctxt.inRoot() ? "object containing " : "") + + value.getClass().getName()+" when strict serialization requested"); + + if (WARNED_CLASSES.add(value.getClass().getCanonicalName())) { + log.warn("Standard serialization not possible for "+value.getClass()+" ("+value+")", error); + } + JsonStreamContext newCtxt = jgen.getOutputContext(); + + // very odd, but flush seems necessary when working with large objects; presumably a buffer which is allowed to clear itself? + // without this, when serializing the large (1.5M) Server json object from BrooklynJacksonSerializerTest creates invalid json, + // containing: "foo":false,"{"error":true,... + jgen.flush(); + + boolean createObject = !newCtxt.inObject() || newCtxt.getCurrentName()!=null; + if (createObject) { + jgen.writeStartObject(); + } + + if (allowEmpty(value.getClass())) { + // write nothing + } else { + + jgen.writeFieldName("error"); + jgen.writeBoolean(true); + + jgen.writeFieldName("errorType"); + jgen.writeString(NotSerializableException.class.getCanonicalName()); + + jgen.writeFieldName("type"); + jgen.writeString(value.getClass().getCanonicalName()); + + jgen.writeFieldName("toString"); + jgen.writeString(value.toString()); + + if (error!=null) { + jgen.writeFieldName("causedByError"); + jgen.writeString(error.toString()); + } + + } + + if (createObject) { + jgen.writeEndObject(); + } + + while (newCtxt!=null && !newCtxt.equals(ctxt)) { + if (jgen.getOutputContext().inArray()) { jgen.writeEndArray(); continue; } + if (jgen.getOutputContext().inObject()) { jgen.writeEndObject(); continue; } + break; + } + + } + + protected boolean allowEmpty(Class<? extends Object> clazz) { + if (clazz.getAnnotation(JsonSerialize.class)!=null && Reflections.hasNoNonObjectFields(clazz)) { + return true; + } else { + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/MultimapSerializer.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/MultimapSerializer.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/MultimapSerializer.java new file mode 100644 index 0000000..1c2f8c1 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/MultimapSerializer.java @@ -0,0 +1,62 @@ +/* + * 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.Collection; +import java.util.Map; + +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.map.SerializerProvider; +import org.codehaus.jackson.map.ser.std.SerializerBase; + +import com.google.common.annotations.Beta; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; + +/** + * Provides a serializer for {@link Multimap} instances. + * <p> + * When Brooklyn's Jackson dependency is updated from org.codehaus.jackson:1.9.13 to + * com.fasterxml.jackson:2.3+ then this class should be replaced with a dependency on + * jackson-datatype-guava and a GuavaModule registered with Brooklyn's ObjectMapper. + */ +@Beta +public class MultimapSerializer extends SerializerBase<Multimap<?, ?>> { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected MultimapSerializer() { + super((Class<Multimap<?, ?>>) (Class) Multimap.class); + } + + @Override + public void serialize(Multimap<?, ?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + writeEntries(value, jgen, provider); + jgen.writeEndObject(); + } + + private void writeEntries(Multimap<?, ?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + for (Map.Entry<?, ? extends Collection<?>> entry : value.asMap().entrySet()) { + provider.findKeySerializer(provider.constructType(String.class), null) + .serialize(entry.getKey(), jgen, provider); + provider.defaultSerializeValue(Lists.newArrayList(entry.getValue()), jgen); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java new file mode 100644 index 0000000..502f063 --- /dev/null +++ b/usage/rest-server/src/main/java/org/apache/brooklyn/rest/util/json/PossiblyStrictPreferringFieldsVisibilityChecker.java @@ -0,0 +1,107 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import org.codehaus.jackson.annotate.JsonAutoDetect; +import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility; +import org.codehaus.jackson.annotate.JsonMethod; +import org.codehaus.jackson.map.introspect.AnnotatedField; +import org.codehaus.jackson.map.introspect.AnnotatedMember; +import org.codehaus.jackson.map.introspect.AnnotatedMethod; +import org.codehaus.jackson.map.introspect.VisibilityChecker; + +/** a visibility checker which disables getters, but allows private access, + * unless {@link BidiSerialization#isStrictSerialization()} is enabled in which case public fields or annotations must be used. + * <p> + * the reason for this change to visibility + * is that getters might generate a copy, resulting in infinite loops, whereas field access should never do so. + * (see e.g. test in {@link BrooklynJacksonSerializerTest} which uses a sensor+config object whose getTypeToken + * causes infinite recursion) + **/ +public class PossiblyStrictPreferringFieldsVisibilityChecker implements VisibilityChecker<PossiblyStrictPreferringFieldsVisibilityChecker> { + VisibilityChecker<?> + vizDefault = new VisibilityChecker.Std(Visibility.NONE, Visibility.NONE, Visibility.NONE, Visibility.ANY, Visibility.ANY), + vizStrict = new VisibilityChecker.Std(Visibility.NONE, Visibility.NONE, Visibility.NONE, Visibility.PUBLIC_ONLY, Visibility.PUBLIC_ONLY); + + @Override public PossiblyStrictPreferringFieldsVisibilityChecker with(JsonAutoDetect ann) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker with(Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withVisibility(JsonMethod method, Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withGetterVisibility(Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withIsGetterVisibility(Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withSetterVisibility(Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withCreatorVisibility(Visibility v) { throw new UnsupportedOperationException(); } + @Override public PossiblyStrictPreferringFieldsVisibilityChecker withFieldVisibility(Visibility v) { throw new UnsupportedOperationException(); } + + protected VisibilityChecker<?> viz() { + return BidiSerialization.isStrictSerialization() ? vizStrict : vizDefault; + } + + @Override public boolean isGetterVisible(Method m) { + return viz().isGetterVisible(m); + } + + @Override + public boolean isGetterVisible(AnnotatedMethod m) { + return isGetterVisible(m.getAnnotated()); + } + + @Override + public boolean isIsGetterVisible(Method m) { + return viz().isIsGetterVisible(m); + } + + @Override + public boolean isIsGetterVisible(AnnotatedMethod m) { + return isIsGetterVisible(m.getAnnotated()); + } + + @Override + public boolean isSetterVisible(Method m) { + return viz().isSetterVisible(m); + } + + @Override + public boolean isSetterVisible(AnnotatedMethod m) { + return isSetterVisible(m.getAnnotated()); + } + + @Override + public boolean isCreatorVisible(Member m) { + return viz().isCreatorVisible(m); + } + + @Override + public boolean isCreatorVisible(AnnotatedMember m) { + return isCreatorVisible(m.getMember()); + } + + @Override + public boolean isFieldVisible(Field f) { + return viz().isFieldVisible(f); + } + + @Override + public boolean isFieldVisible(AnnotatedField f) { + return isFieldVisible(f.getAnnotated()); + } +} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/main/webapp/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/main/webapp/WEB-INF/web.xml b/usage/rest-server/src/main/webapp/WEB-INF/web.xml index 9cbd5d7..2e0aeda 100644 --- a/usage/rest-server/src/main/webapp/WEB-INF/web.xml +++ b/usage/rest-server/src/main/webapp/WEB-INF/web.xml @@ -24,7 +24,7 @@ <filter> <filter-name>Brooklyn Request Tagging Filter</filter-name> - <filter-class>brooklyn.rest.filter.RequestTaggingFilter</filter-class> + <filter-class>org.apache.brooklyn.rest.filter.RequestTaggingFilter</filter-class> </filter> <filter-mapping> <filter-name>Brooklyn Request Tagging Filter</filter-name> @@ -33,7 +33,7 @@ <filter> <filter-name>Brooklyn Properties Authentication Filter</filter-name> - <filter-class>brooklyn.rest.filter.BrooklynPropertiesSecurityFilter</filter-class> + <filter-class>org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter</filter-class> </filter> <filter-mapping> <filter-name>Brooklyn Properties Authentication Filter</filter-name> @@ -42,7 +42,7 @@ <filter> <filter-name>Brooklyn Logging Filter</filter-name> - <filter-class>brooklyn.rest.filter.LoggingFilter</filter-class> + <filter-class>org.apache.brooklyn.rest.filter.LoggingFilter</filter-class> </filter> <filter-mapping> <filter-name>Brooklyn Logging Filter</filter-name> @@ -51,7 +51,7 @@ <filter> <filter-name>Brooklyn HA Master Filter</filter-name> - <filter-class>brooklyn.rest.filter.HaMasterCheckFilter</filter-class> + <filter-class>org.apache.brooklyn.rest.filter.HaMasterCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>Brooklyn HA Master Filter</filter-name> @@ -81,22 +81,22 @@ <param-name>com.sun.jersey.config.property.classnames</param-name> <param-value> brooklyn.rest.apidoc.ApidocHelpMessageBodyWriter; - brooklyn.rest.util.FormMapProvider; + org.apache.brooklyn.rest.util.FormMapProvider; org.codehaus.jackson.jaxrs.JacksonJsonProvider; - brooklyn.rest.resources.ActivityResource; - brooklyn.rest.resources.ApidocResource; - brooklyn.rest.resources.ApplicationResource; - brooklyn.rest.resources.CatalogResource; - brooklyn.rest.resources.EffectorResource; - brooklyn.rest.resources.EntityConfigResource; - brooklyn.rest.resources.EntityResource; - brooklyn.rest.resources.LocationResource; - brooklyn.rest.resources.PolicyConfigResource; - brooklyn.rest.resources.PolicyResource; - brooklyn.rest.resources.ScriptResource; - brooklyn.rest.resources.SensorResource; - brooklyn.rest.resources.UsageResource; - brooklyn.rest.resources.VersionResource; + org.apache.brooklyn.rest.resources.ActivityResource; + org.apache.brooklyn.rest.resources.ApidocResource; + org.apache.brooklyn.rest.resources.ApplicationResource; + org.apache.brooklyn.rest.resources.CatalogResource; + org.apache.brooklyn.rest.resources.EffectorResource; + org.apache.brooklyn.rest.resources.EntityConfigResource; + org.apache.brooklyn.rest.resources.EntityResource; + org.apache.brooklyn.rest.resources.LocationResource; + org.apache.brooklyn.rest.resources.PolicyConfigResource; + org.apache.brooklyn.rest.resources.PolicyResource; + org.apache.brooklyn.rest.resources.ScriptResource; + org.apache.brooklyn.rest.resources.SensorResource; + org.apache.brooklyn.rest.resources.UsageResource; + org.apache.brooklyn.rest.resources.VersionResource; </param-value> </init-param> http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/test/java/brooklyn/config/render/TestRendererHints.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/brooklyn/config/render/TestRendererHints.java b/usage/rest-server/src/test/java/brooklyn/config/render/TestRendererHints.java index 13860d7..131866c 100644 --- a/usage/rest-server/src/test/java/brooklyn/config/render/TestRendererHints.java +++ b/usage/rest-server/src/test/java/brooklyn/config/render/TestRendererHints.java @@ -18,6 +18,8 @@ */ package brooklyn.config.render; +import brooklyn.config.render.RendererHints; + /** Methods used when testing the {@link RendererHints} regiostry. */ public class TestRendererHints { http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/test/java/brooklyn/entity/brooklynnode/DeployBlueprintTest.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/brooklyn/entity/brooklynnode/DeployBlueprintTest.java b/usage/rest-server/src/test/java/brooklyn/entity/brooklynnode/DeployBlueprintTest.java deleted file mode 100644 index 42f0785..0000000 --- a/usage/rest-server/src/test/java/brooklyn/entity/brooklynnode/DeployBlueprintTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 brooklyn.entity.brooklynnode; - -import static org.testng.Assert.assertEquals; - -import java.net.URI; -import java.util.List; -import java.util.Map; - -import org.eclipse.jetty.server.Server; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import brooklyn.entity.basic.BasicApplication; -import brooklyn.entity.basic.EntityLocal; -import brooklyn.entity.brooklynnode.BrooklynNode.DeployBlueprintEffector; -import brooklyn.entity.proxying.EntitySpec; -import brooklyn.event.feed.http.JsonFunctions; -import brooklyn.management.EntityManager; -import brooklyn.rest.BrooklynRestApiLauncherTestFixture; -import brooklyn.test.HttpTestUtils; -import brooklyn.util.guava.Functionals; - -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -public class DeployBlueprintTest extends BrooklynRestApiLauncherTestFixture { - - private static final Logger log = LoggerFactory.getLogger(DeployBlueprintTest.class); - - Server server; - - @BeforeMethod(alwaysRun=true) - public void setUp() throws Exception { - server = newServer(); - useServerForTest(server); - } - - @Test - public void testStartsAppViaEffector() throws Exception { - URI webConsoleUri = URI.create(getBaseUri()); - - EntitySpec<BrooklynNode> spec = EntitySpec.create(BrooklynNode.class); - EntityManager mgr = getManagementContextFromJettyServerAttributes(server).getEntityManager(); - BrooklynNode node = mgr.createEntity(spec); - ((EntityLocal)node).setAttribute(BrooklynNode.WEB_CONSOLE_URI, webConsoleUri); - mgr.manage(node); - Map<String, String> params = ImmutableMap.of(DeployBlueprintEffector.BLUEPRINT_CAMP_PLAN.getName(), "{ services: [ serviceType: \"java:"+BasicApplication.class.getName()+"\" ] }"); - String id = node.invoke(BrooklynNode.DEPLOY_BLUEPRINT, params).getUnchecked(); - - log.info("got: "+id); - - String apps = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications"); - List<String> appType = parseJsonList(apps, ImmutableList.of("spec", "type"), String.class); - assertEquals(appType, ImmutableList.of(BasicApplication.class.getName())); - - String status = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications/"+id+"/entities/"+id+"/sensors/service.status"); - log.info("STATUS: "+status); - } - - private <T> List<T> parseJsonList(String json, List<String> elements, Class<T> clazz) { - Function<String, List<T>> func = Functionals.chain( - JsonFunctions.asJson(), - JsonFunctions.forEach(Functionals.chain( - JsonFunctions.walk(elements), - JsonFunctions.cast(clazz)))); - return func.apply(json); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a9e5ca55/usage/rest-server/src/test/java/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java ---------------------------------------------------------------------- diff --git a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java b/usage/rest-server/src/test/java/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java deleted file mode 100644 index f9fa288..0000000 --- a/usage/rest-server/src/test/java/brooklyn/rest/BrooklynPropertiesSecurityFilterTest.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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 brooklyn.rest; - -import static org.testng.Assert.assertTrue; - -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.http.HttpHeaders; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.entity.ContentType; -import org.apache.http.message.BasicNameValuePair; -import org.eclipse.jetty.server.Server; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testng.annotations.Test; - -import brooklyn.rest.security.provider.AnyoneSecurityProvider; -import brooklyn.util.collections.MutableMap; -import brooklyn.util.http.HttpTool; -import brooklyn.util.http.HttpToolResponse; -import brooklyn.util.time.Time; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Charsets; -import com.google.common.base.Stopwatch; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; - -public class BrooklynPropertiesSecurityFilterTest extends BrooklynRestApiLauncherTestFixture { - - private static final Logger LOG = LoggerFactory.getLogger(BrooklynPropertiesSecurityFilterTest.class); - - // Would be great for this to be a unit test but it takes almost ten seconds. - @Test(groups = "Integration") - public void testInteractionOfSecurityFilterAndFormMapProvider() throws Exception { - Stopwatch stopwatch = Stopwatch.createStarted(); - try { - Server server = useServerForTest(BrooklynRestApiLauncher.launcher() - .securityProvider(AnyoneSecurityProvider.class) - .forceUseOfDefaultCatalogWithJavaClassPath(true) - .withoutJsgui() - .start()); - String appId = startAppAtNode(server); - String entityId = getTestEntityInApp(server, appId); - HttpClient client = HttpTool.httpClientBuilder() - .uri(getBaseUri(server)) - .build(); - List<? extends NameValuePair> nvps = Lists.newArrayList( - new BasicNameValuePair("arg", "bar")); - String effector = String.format("/v1/applications/%s/entities/%s/effectors/identityEffector", appId, entityId); - HttpToolResponse response = HttpTool.httpPost(client, URI.create(getBaseUri() + effector), - ImmutableMap.of(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()), - URLEncodedUtils.format(nvps, Charsets.UTF_8).getBytes()); - - LOG.info("Effector response: {}", response.getContentAsString()); - assertTrue(HttpTool.isStatusCodeHealthy(response.getResponseCode()), "response code=" + response.getResponseCode()); - } finally { - LOG.info("testInteractionOfSecurityFilterAndFormMapProvider complete in " + Time.makeTimeStringRounded(stopwatch)); - } - } - - private String startAppAtNode(Server server) throws Exception { - String blueprint = "name: TestApp\n" + - "location: localhost\n" + - "services:\n" + - "- type: brooklyn.test.entity.TestEntity"; - HttpClient client = HttpTool.httpClientBuilder() - .uri(getBaseUri(server)) - .build(); - HttpToolResponse response = HttpTool.httpPost(client, URI.create(getBaseUri() + "/v1/applications"), - ImmutableMap.of(HttpHeaders.CONTENT_TYPE, "application/x-yaml"), - blueprint.getBytes()); - assertTrue(HttpTool.isStatusCodeHealthy(response.getResponseCode()), "error creating app. response code=" + response.getResponseCode()); - @SuppressWarnings("unchecked") - Map<String, Object> body = new ObjectMapper().readValue(response.getContent(), HashMap.class); - return (String) body.get("entityId"); - } - - @SuppressWarnings("rawtypes") - private String getTestEntityInApp(Server server, String appId) throws Exception { - HttpClient client = HttpTool.httpClientBuilder() - .uri(getBaseUri(server)) - .build(); - List entities = new ObjectMapper().readValue( - HttpTool.httpGet(client, URI.create(getBaseUri() + "/v1/applications/" + appId + "/entities"), MutableMap.<String, String>of()).getContent(), List.class); - LOG.info((String) ((Map) entities.get(0)).get("id")); - return (String) ((Map) entities.get(0)).get("id"); - } -}
