/* * #%L * FlatPack Jersey integration * %% * Copyright (C) 2012 Perka Inc. * %% * 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. * #L% */ package com.getperka.flatpack.jersey; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.security.Principal; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.Providers; import com.getperka.flatpack.FlatPack; import com.getperka.flatpack.FlatPackEntity; import com.getperka.flatpack.util.IoObserver; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.sun.jersey.spi.container.ContainerRequest; import com.sun.jersey.spi.container.ContainerRequestFilter; import com.sun.jersey.spi.container.ContainerResponse; import com.sun.jersey.spi.container.ContainerResponseFilter; /** * Adapts the FlatPack serialization mechanisms to the Jersey / jax-rs stack. */ @Provider @Consumes({ MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN }) @Produces({ MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN }) public class FlatPackProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object>, ContainerRequestFilter, ContainerResponseFilter { private static final Charset UTF8 = Charset.forName("UTF-8"); private static final ThreadLocal<Principal> requestPrincipal = new ThreadLocal<Principal>(); private static final ThreadLocal<Map<String, String>> flatpackWarnings = new ThreadLocal<Map<String, String>>(); @Context Providers providers; private FlatPack flatpack; private IoObserver observer = new IoObserver.Null(); /** * Capture the Principal associated with the current thread for use by the post-request filter * method. */ @Override public ContainerRequest filter(ContainerRequest request) { requestPrincipal.set(request.getUserPrincipal()); return request; } /** * This method will serialize a FlatPackEntity, rather than waiting for {@link #writeTo}. This is * because {@code writeTo} will be called after all of the ContainerResponseFilters have been * called, which may prevent some objects from being serialized if they depend on container state. */ @Override public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { try { if (!MediaType.APPLICATION_JSON_TYPE.equals(response.getMediaType())) { return response; } Object t = response.getEntity(); if (t == null) { // No data, nothing to do return response; } FlatPackEntity<?> toSend; if (t instanceof FlatPackEntity) { toSend = (FlatPackEntity<?>) t; } else { // Possibly wrap a flatpack-able type in a FlatPackEntity Type type = response.getEntityType(); if (type == null) { type = t.getClass(); } if (getFlatPack().isRootType(type)) { Principal principal = request.getUserPrincipal(); toSend = FlatPackEntity.create(type, t, principal); } else { toSend = null; } } // The response isn't something that should be packed, so just let it through if (toSend == null) { return response; } // Copy and thread-local warnings into the output Map<String, String> warnings = flatpackWarnings.get(); if (warnings != null) { for (Map.Entry<String, String> entry : warnings.entrySet()) { toSend.addWarning(entry.getKey(), entry.getValue()); } } // Create the payload StringWriter out = new StringWriter(); try { getFlatPack().getPacker().pack(toSend, observer.observe(out)); } catch (IOException e) { throw new WebApplicationException(e); } // Swap out the response's entity response.setEntity(out.toString(), String.class); return response; } finally { requestPrincipal.remove(); flatpackWarnings.remove(); } } @Override public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } @Override public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return FlatPackEntity.class.equals(type) || JsonElement.class.isAssignableFrom(type) || getFlatPack().isRootType(genericType); } @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return FlatPackEntity.class.isAssignableFrom(type) || JsonElement.class.isAssignableFrom(type) || getFlatPack().isRootType(genericType); } @Override public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { if (InputStream.class.equals(type)) { return entityStream; } Reader in = observer.observe(new InputStreamReader(entityStream, UTF8)); if (Reader.class.equals(type)) { return in; } if (JsonElement.class.isAssignableFrom(type)) { try { return new JsonParser().parse(in); } finally { in.close(); } } FlatPackEntity<?> entity; Object toReturn; if (FlatPackEntity.class.equals(type)) { Type parameterization; if (genericType instanceof ParameterizedType) { parameterization = ((ParameterizedType) genericType).getActualTypeArguments()[0]; } else { parameterization = Void.class; } entity = getFlatPack().getUnpacker().unpack(parameterization, in, requestPrincipal.get()); toReturn = entity; } else { entity = getFlatPack().getUnpacker().unpack(genericType, in, requestPrincipal.get()); toReturn = entity.getValue(); } in.close(); flatpackWarnings.set(entity.getExtraWarnings()); return toReturn; } public void setObserver(IoObserver observer) { this.observer = observer; } /** * This method generally shouldn't be called on the server, since the * {@link #filter(ContainerRequest, ContainerResponse)} method above should have already * serialized the response. It is, however, used when using {@code jersey-client} code. */ @Override public void writeTo(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { Writer writer = observer.observe(new OutputStreamWriter(entityStream, UTF8)); try { if (t instanceof JsonElement) { new Gson().toJson((JsonElement) t, writer); } else if (t instanceof String) { writer.write((String) t); } else if (t instanceof FlatPackEntity) { FlatPackEntity<?> fpe = (FlatPackEntity<?>) t; getFlatPack().getPacker().pack(fpe, writer); } else if (getFlatPack().isRootType(genericType)) { FlatPackEntity<?> fpe = FlatPackEntity.create(genericType, t, null); getFlatPack().getPacker().pack(fpe, writer); } else { // Indicates an error in isWritable() throw new UnsupportedOperationException("Cannot write a " + t.getClass().getName()); } } finally { writer.close(); } } private FlatPack getFlatPack() { if (flatpack == null) { flatpack = providers.getContextResolver(FlatPack.class, MediaType.WILDCARD_TYPE) .getContext(FlatPack.class); } return flatpack; } }