package org.jboss.resteasy.plugins.providers.jackson; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import javax.ws.rs.ConstrainedTo; import javax.ws.rs.RuntimeType; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.WriterInterceptor; import javax.ws.rs.ext.WriterInterceptorContext; import com.fasterxml.jackson.databind.ObjectMapper; import org.jboss.resteasy.core.MediaTypeMap; import org.jboss.resteasy.spi.ResteasyConfiguration; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.util.CommitHeaderOutputStream; import org.jboss.resteasy.resteasy_jaxrs.i18n.*; /** * <p> * JSONP is an alternative to normal AJAX requests. Instead of using a XMLHttpRequest a script tag is added to the DOM. * The browser will call the corresponding URL and download the JavaScript. The server creates a response which looks like a * method call. The parameter is the body of the request. The name of the method to call is normally passed as query parameter. * The method has to be present in the current JavaScript environment. * </p> * <p> * Jackson JSON processor can produce such an response. This interceptor checks if the media type is a JavaScript one if there is a query * parameter with the method name. The default name of this query parameter is "callback". So this interceptor is compatible with * <a href="http://api.jquery.com/jQuery.ajax/">jQuery</a>. * </p> * <p> * It is possible to wrap the generated javascript function call in a try-catch block. * You can enable it either by setting the {@link #wrapInTryCatch} property of the provider instance to {@code true} * or by setting the {@code resteasy.jsonp.silent} context-param to true: * </p> * <pre> * {@code * <context-param> * <param-name>resteasy.jsonp.silent</param-name> * <param-value>true</param-value> * </context-param> * } * </pre> * * @author <a href="mailto:holger.morch@nokia.com">Holger Morch</a> * @version $Revision: 1 $ */ @Provider @ConstrainedTo(RuntimeType.SERVER) public class Jackson2JsonpInterceptor implements WriterInterceptor{ /** * "text/javascript" media type. Default media type of script tags. */ public static final MediaType TEXT_JAVASCRIPT_MEDIA_TYPE = new MediaType("text", "javascript"); /** * "application/javascript" media type. */ public static final MediaType APPLICATION_JAVASCRIPT_MEDIA_TYPE = new MediaType("application", "javascript"); /** * "text/json" media type. */ public static final MediaType TEXT_JSON_TYPE = new MediaType("text", "json"); /** * "application/*+json" media type. */ public static final MediaType APPLICATION_PLUS_JSON_TYPE = new MediaType("application", "*+json"); /** * Default name of the query parameter with the method name. */ public static final String DEFAULT_CALLBACK_QUERY_PARAMETER = "callback"; /** * If response media type is one of this jsonp response may be created. */ public static final MediaTypeMap<String> jsonpCompatibleMediaTypes = new MediaTypeMap<String>(); /** * Default {@link ObjectMapper} for type resolution. Used if none is provided by {@link Providers}. */ protected static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper(); static { jsonpCompatibleMediaTypes.add(MediaType.APPLICATION_JSON_TYPE , MediaType.APPLICATION_JSON_TYPE.toString()); jsonpCompatibleMediaTypes.add(APPLICATION_JAVASCRIPT_MEDIA_TYPE, APPLICATION_JAVASCRIPT_MEDIA_TYPE.toString()); jsonpCompatibleMediaTypes.add(APPLICATION_PLUS_JSON_TYPE , APPLICATION_PLUS_JSON_TYPE.toString()); jsonpCompatibleMediaTypes.add(TEXT_JSON_TYPE , TEXT_JSON_TYPE.toString()); jsonpCompatibleMediaTypes.add(TEXT_JAVASCRIPT_MEDIA_TYPE , TEXT_JAVASCRIPT_MEDIA_TYPE.toString()); } private UriInfo uri; private String callbackQueryParameter = DEFAULT_CALLBACK_QUERY_PARAMETER; private boolean wrapInTryCatch = false; public Jackson2JsonpInterceptor() { ResteasyConfiguration context = ResteasyProviderFactory.getContextData(ResteasyConfiguration.class); if (context != null) { wrapInTryCatch = Boolean.parseBoolean(context.getParameter("resteasy.jsonp.silent")); enabled = Boolean.parseBoolean(context.getParameter("resteasy.jsonp.enable")); } } /** * The {@link ObjectMapper} used to create typing information. */ protected ObjectMapper objectMapper; /** * The {@link Providers} used to retrieve the {@link #objectMapper} from. */ protected Providers providers; /** * Is this interceptor enabled. */ private boolean enabled = false; /** * This subclass of {@link CommitHeaderOutputStream} overrides the {@link #close()} method so it would commit * the headers only, without actually calling the {@link #close()} method of the delegate {@link OutputStream} */ private static class DoNotCloseDelegateOutputStream extends BufferedOutputStream { public DoNotCloseDelegateOutputStream(OutputStream delegate) { super(delegate); } @Override public void close() throws IOException { flush(); } } /** * {@inheritDoc} */ @Override public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { LogMessages.LOGGER.debugf("Interceptor : %s, Method : aroundWriteTo", getClass().getName()); String function = uri.getQueryParameters().getFirst(callbackQueryParameter); if (enabled && function != null && !function.trim().isEmpty() && !jsonpCompatibleMediaTypes.getPossible(context.getMediaType()).isEmpty()){ OutputStreamWriter writer = new OutputStreamWriter(context.getOutputStream()); if (wrapInTryCatch) writer.write("try{"); writer.write(function + "("); writer.flush(); // Disable the close method before calling context.proceed() OutputStream old = context.getOutputStream(); DoNotCloseDelegateOutputStream wrappedOutputStream = new DoNotCloseDelegateOutputStream(old); context.setOutputStream(wrappedOutputStream); try { context.proceed(); wrappedOutputStream.flush(); writer.write(")"); if (wrapInTryCatch) writer.write("}catch(e){}"); writer.flush(); } finally { context.setOutputStream(old); } } else { context.proceed(); } } /** * Search for an {@link ObjectMapper} for the given class and mediaType * * @param type the {@link Class} to serialize * @param mediaType the response {@link MediaType} * @return the {@link ObjectMapper} */ protected ObjectMapper getObjectMapper(Class<?> type, MediaType mediaType) { if (objectMapper != null) { return objectMapper; } if (providers != null) { ContextResolver<ObjectMapper> resolver = providers.getContextResolver(ObjectMapper.class, mediaType); if (resolver == null) { resolver = providers.getContextResolver(ObjectMapper.class, null); } if (resolver != null) { return resolver.getContext(type); } } return DEFAULT_MAPPER; } /** * Setter used by RESTeasy to provide the {@link UriInfo}. * * @param uri the uri to set */ @Context public void setUri(UriInfo uri) { this.uri = uri; } /** * Setter used by RESTeasy to provide the {@link Providers} * * @param providers */ @Context public void setProviders(Providers providers) { this.providers = providers; } /** * Set an fix {@link ObjectMapper}. If this is not set {@link Providers} are used for lookup. If there are is none too, use a default one. * * @param objectMapper */ public void setObjectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } /** * Get the name of the query parameter which contains the JavaScript method name. Default: callback. * * @return the callbackQueryParameter */ public String getCallbackQueryParameter() { return callbackQueryParameter; } /** * Set callback query parameter. * * @see #getCallbackQueryParameter() * @param callbackQueryParameter the callbackQueryParameter to set */ public void setCallbackQueryParameter(String callbackQueryParameter) { this.callbackQueryParameter = callbackQueryParameter; } /** * Check is the JSONP callback will be wrapped with try-catch block * * @return true if try-catch block is generated; false otherwise */ public boolean isWrapInTryCatch() { return wrapInTryCatch; } /** * Enables or disables wrapping the JSONP callback try try-catch block * * @param wrapInTryCatch true if you want to wrap the result with try-catch block; false otherwise */ public void setWrapInTryCatch(boolean wrapInTryCatch) { this.wrapInTryCatch = wrapInTryCatch; } }