package org.codehaus.jackson.jaxrs;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.*;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.ext.*;
import org.codehaus.jackson.*;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.type.ClassKey;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
/**
* Basic implementation of JAX-RS abstractions ({@link MessageBodyReader},
* {@link MessageBodyWriter}) needed for binding
* JSON ("application/json") content to and from POJOs.
*
* Currently most configurability is via caller configuring
* {@link ObjectMapper} it uses to construct this provider.
* Additionally it is possible to enable detection of which types
* can be serialized/deserialized, which is not enabled by default
* (since it is usually not needed).
*
* @author Tatu Saloranta
*/
@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class JacksonJsonProvider
implements
MessageBodyReader,
MessageBodyWriter
{
/**
* 06-Apr-2009, tatu: Looks like we need to worry about accidental
* data binding for types we shouldn't be handling. This is
* probably not a very good way to do it, but let's start by
* blacklisting things we are not to handle.
*
* (why ClassKey? since plain old Class has no hashCode() defined,
* lookups are painfully slow)
*/
public final static HashSet _untouchables = new HashSet();
static {
// First, I/O things (direct matches)
_untouchables.add(new ClassKey(java.io.InputStream.class));
_untouchables.add(new ClassKey(java.io.Reader.class));
_untouchables.add(new ClassKey(java.io.OutputStream.class));
_untouchables.add(new ClassKey(java.io.Writer.class));
// then some primitive types
_untouchables.add(new ClassKey(byte[].class));
_untouchables.add(new ClassKey(char[].class));
// 24-Apr-2009, tatu: String is an edge case... let's leave it out
_untouchables.add(new ClassKey(String.class));
// Then core JAX-RS things
_untouchables.add(new ClassKey(StreamingOutput.class));
_untouchables.add(new ClassKey(Response.class));
}
public final static Class>[] _unreadableClasses = new Class>[] {
InputStream.class, Reader.class
};
public final static Class>[] _unwritableClasses = new Class>[] {
OutputStream.class, Writer.class,
StreamingOutput.class, Response.class
};
/**
* Default ObjectMapper to use if none is configured for provider
* to use.
*/
protected final static ObjectMapper _defaultMapper = new ObjectMapper();
/*
///////////////////////////////////////////////////////
// Context configuration
///////////////////////////////////////////////////////
*/
/**
* Injectable context object used to locate configured
* instance of {@link ObjectMapper} to use for actual
* serialization.
*/
@Context
protected Providers _providers;
/**
* Mapper provider was constructed with if any; if null, provider
* is dynamically located; and if that fails, default instance
* will be used.
*/
protected ObjectMapper _configuredMapper;
/*
///////////////////////////////////////////////////////
// Configuration
///////////////////////////////////////////////////////
*/
/**
* Whether return type of type String is to be output
* as JSON strings (double-quoted) or not. Default to "false",
* as most often this is not wanted
*/
protected boolean _cfgSerializeStringAsJSON = false;
/**
* Whether we want to actually check that Jackson has
* a serializer for given type. Since this should generally
* be the case (due to auto-discovery) and since the call
* to check this is not free, defaults to false.
*/
protected boolean _cfgCheckCanSerialize = false;
/**
* Whether we want to actually check that Jackson has
* a deserializer for given type. Since this should generally
* be the case (due to auto-discovery) and since the call
* to check this is not free, defaults to false.
*/
protected boolean _cfgCheckCanDeserialize = false;
/*
///////////////////////////////////////////////////////
// Construction
///////////////////////////////////////////////////////
*/
/**
* Default constructor, usually used when provider is automatically
* configured to be used with JAX-RS implementation.
*/
public JacksonJsonProvider()
{
this(null);
}
/**
* Constructor to use when a custom mapper (usually components
* like serializer/deserializer factories that have been configured)
* is to be used.
*/
public JacksonJsonProvider(ObjectMapper mapper)
{
_configuredMapper = mapper;
}
/*
///////////////////////////////////////////////////////
// Configuring
///////////////////////////////////////////////////////
*/
public void checkCanDeserialize(boolean state) { _cfgCheckCanDeserialize = state; }
public void checkCanSerialize(boolean state) { _cfgCheckCanSerialize = state; }
/**
* Method for enabling/disabling providers conversion of plain old Strings
* to JSON Strings; affects both input and output data binding.
*/
public void serializeStringsAsJSON(boolean state) {
_cfgSerializeStringAsJSON = state;
}
/*
////////////////////////////////////////////////////
// MessageBodyReader impl
////////////////////////////////////////////////////
*/
public boolean isReadable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
if (!_isJsonType(mediaType)) {
return false;
}
/* Ok: looks like we must weed out some core types here; ones that
* make no sense to try to bind from JSON:
*/
if (_untouchables.contains(new ClassKey(type))) {
return false;
}
// but some are interface/abstract classes, so
for (Class> cls : _unreadableClasses) {
if (cls.isAssignableFrom(type)) {
return false;
}
}
// Finally: if we really want to verify that we can serialize, we'll check:
if (_cfgCheckCanSerialize) {
if (!_getMapper(type, mediaType).canDeserialize(_convertType(type))) {
return false;
}
}
return true;
}
public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream)
throws IOException
{
ObjectMapper mapper = _getMapper(type, mediaType);
JsonParser jp = mapper.getJsonFactory().createJsonParser(entityStream);
/* Important: we are NOT to close the underlying stream after
* mapping, so we need to instruct parser:
*/
jp.disableFeature(JsonParser.Feature.AUTO_CLOSE_SOURCE);
return mapper.readValue(jp, _convertType(genericType));
}
/*
////////////////////////////////////////////////////
// MessageBodyWriter impl
////////////////////////////////////////////////////
*/
public long getSize(Object value, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
/* In general figuring output size requires actual writing; usually not
* worth it to write everything twice.
*/
return -1;
}
public boolean isWriteable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
if (!_isJsonType(mediaType)) {
return false;
}
/* Ok: looks like we must weed out some core types here; ones that
* make no sense to try to bind from JSON:
*/
if (_untouchables.contains(new ClassKey(type))) {
return false;
}
// but some are interface/abstract classes, so
for (Class> cls : _unwritableClasses) {
if (cls.isAssignableFrom(type)) {
return false;
}
}
// Also: if we really want to verify that we can deserialize, we'll check:
if (_cfgCheckCanSerialize) {
if (!_getMapper(type, mediaType).canSerialize(type)) {
return false;
}
}
return true;
}
public void writeTo(Object value, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream)
throws IOException
{
/* 27-Feb-2009, tatu: Where can we find desired encoding? Within
* http headers?
*/
ObjectMapper mapper = _getMapper(type, mediaType);
JsonGenerator jg = mapper.getJsonFactory().createJsonGenerator(entityStream, JsonEncoding.UTF8);
jg.disableFeature(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
// Want indentation?
if (mapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
jg.useDefaultPrettyPrinter();
}
mapper.writeValue(jg, value);
}
/*
////////////////////////////////////////////////////
// Helper methods
////////////////////////////////////////////////////
*/
/**
* @param type Class of object being serialized or deserialized;
* not used at this point, since it is assumed that unprocessable
* classes have been already weeded out
*/
protected ObjectMapper _getMapper(Class> type, MediaType mediaType)
{
// First: were we configured with an instance?
if (_configuredMapper != null) {
return _configuredMapper;
}
// If not, maybe we can get one configured via context?
ContextResolver resolver = _providers.getContextResolver(ObjectMapper.class, mediaType);
// Not registered with media type, how about without?
if (resolver == null) {
resolver = _providers.getContextResolver(ObjectMapper.class, null);
}
if (resolver != null) {
ObjectMapper mapper = resolver.getContext(ObjectMapper.class);
if (mapper != null) {
return mapper;
}
}
// nope: must use globally shared instance
return _defaultMapper;
}
protected boolean _isJsonType(MediaType mediaType)
{
/* As suggested by Stephen D, there are 2 ways to check: either
* being as inclusive as possible (if subtype is "json"), or
* exclusive (major type "application", minor type "json").
* Let's start with inclusive one, hard to know which major
* types we should cover aside from "application".
*/
if (mediaType != null) {
// Ok: there are also "xxx+json" subtypes, which count as well
String subtype = mediaType.getSubtype();
return "json".equals(subtype) || subtype.endsWith("+json");
}
return false;
}
/**
* Method used to construct a JDK generic type into type definition
* Jackson understands.
*/
protected JavaType _convertType(Type jdkType)
{
return TypeFactory.fromType(jdkType);
}
}