This is an automated email from the git hooks/post-receive script. tjaalton pushed a commit to branch master in repository jackson-jaxrs-providers.
commit e8d9040cb51c5649a886c97a78d0a8554b24714b Author: Tatu Saloranta <[email protected]> Date: Mon Feb 4 14:26:31 2013 -0800 first version with full json provider --- .../fasterxml/jackson/jaxrs/json/Annotations.java | 21 + .../jaxrs/json/JacksonJaxbJsonProvider.java | 64 ++ .../jackson/jaxrs/json/JacksonJsonProvider.java | 724 +++++++++++++++++++++ .../jaxrs/json/JsonMappingExceptionMapper.java | 18 + .../jaxrs/json/JsonParseExceptionMapper.java | 18 + .../jackson/jaxrs/json/PackageVersion.java.in | 20 + .../jaxrs/json/annotation/EndpointConfig.java | 214 ++++++ .../jackson/jaxrs/json/annotation/JSONP.java | 94 +++ .../jaxrs/json/annotation/JacksonFeatures.java | 39 ++ .../jaxrs/json/annotation/package-info.java | 5 + .../jackson/jaxrs/json/cfg/MapperConfigurator.java | 180 +++++ .../fasterxml/jackson/jaxrs/json/package-info.java | 21 + .../jaxrs/json/util/AnnotationBundleKey.java | 110 ++++ .../jackson/jaxrs/json/util/ClassKey.java | 92 +++ .../fasterxml/jackson/jaxrs/json/util/LRUMap.java | 26 + .../services/javax.ws.rs.ext.MessageBodyReader | 1 + .../services/javax.ws.rs.ext.MessageBodyWriter | 1 + .../jackson/jaxrs/json/JaxrsTestBase.java | 92 +++ .../jackson/jaxrs/json/TestCanDeserialize.java | 43 ++ .../jackson/jaxrs/json/TestCanSerialize.java | 46 ++ .../jackson/jaxrs/json/TestJacksonFeatures.java | 111 ++++ .../fasterxml/jackson/jaxrs/json/TestJsonView.java | 46 ++ .../jackson/jaxrs/json/TestJsonpWrapping.java | 27 + .../fasterxml/jackson/jaxrs/json/TestRootType.java | 45 ++ .../jackson/jaxrs/json/TestStreamingOutput.java | 33 + .../jackson/jaxrs/json/TestUntouchables.java | 80 +++ .../fasterxml/jackson/jaxrs/json/TestVersions.java | 28 + .../jaxrs/json/util/TestAnnotationBundleKey.java | 56 ++ 28 files changed, 2255 insertions(+) diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/Annotations.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/Annotations.java new file mode 100644 index 0000000..cd536d8 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/Annotations.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.jaxrs.json; + +/** + * Enumeration that defines standard annotation sets available for configuring + * data binding aspects. + */ +public enum Annotations { + /** + * Standard Jackson annotations, defined in Jackson core and databind + * packages + */ + JACKSON, + + /** + * Standard JAXB annotations, used in a way that approximates expected + * definitions (since JAXB defines XML aspects, not all features map + * well to JSON handling) + */ + JAXB + ; +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJaxbJsonProvider.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJaxbJsonProvider.java new file mode 100644 index 0000000..d6b2473 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJaxbJsonProvider.java @@ -0,0 +1,64 @@ +package com.fasterxml.jackson.jaxrs.json; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * JSON content type provider automatically configured to use both Jackson + * and JAXB annotations (in that order of priority). Otherwise functionally + * same as {@link JacksonJsonProvider}. + *<p> + * Typical usage pattern is to just instantiate instance of this + * provider for JAX-RS and use as is: this will use both Jackson and + * JAXB annotations (with Jackson annotations having priority). + *<p> + * Note: class annotations are duplicated from super class, since it + * is not clear whether JAX-RS implementations are required to + * check settings of super-classes. It is important to keep annotations + * in sync if changed. + */ +@Provider +@Consumes(MediaType.WILDCARD) // NOTE: required to support "non-standard" JSON variants +@Produces(MediaType.WILDCARD) +public class JacksonJaxbJsonProvider extends JacksonJsonProvider { + /** + * Default annotation sets to use, if not explicitly defined during + * construction: use Jackson annotations if found; if not, use + * JAXB annotations as fallback. + */ + public final static Annotations[] DEFAULT_ANNOTATIONS = { + Annotations.JACKSON, Annotations.JAXB + }; + + /** + * Default constructor, usually used when provider is automatically + * configured to be used with JAX-RS implementation. + */ + public JacksonJaxbJsonProvider() + { + this(null, DEFAULT_ANNOTATIONS); + } + + /** + * @param annotationsToUse Annotation set(s) to use for configuring + * data binding + */ + public JacksonJaxbJsonProvider(Annotations... annotationsToUse) + { + this(null, annotationsToUse); + } + + /** + * Constructor to use when a custom mapper (usually components + * like serializer/deserializer factories that have been configured) + * is to be used. + */ + public JacksonJaxbJsonProvider(ObjectMapper mapper, Annotations[] annotationsToUse) + { + super(mapper, annotationsToUse); + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJsonProvider.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJsonProvider.java new file mode 100644 index 0000000..a388b3f --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JacksonJsonProvider.java @@ -0,0 +1,724 @@ +package com.fasterxml.jackson.jaxrs.json; + +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 com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.util.LRUMap; +import com.fasterxml.jackson.jaxrs.json.annotation.EndpointConfig; +import com.fasterxml.jackson.jaxrs.json.cfg.MapperConfigurator; +import com.fasterxml.jackson.jaxrs.json.util.AnnotationBundleKey; +import com.fasterxml.jackson.jaxrs.json.util.ClassKey; + +/** + * Basic implementation of JAX-RS abstractions ({@link MessageBodyReader}, + * {@link MessageBodyWriter}) needed for binding + * JSON ("application/json") content to and from Java Objects ("POJO"s). + *<p> + * Actual data binding functionality is implemented by {@link ObjectMapper}: + * mapper to use can be configured in multiple ways: + * <ul> + * <li>By explicitly passing mapper to use in constructor + * <li>By explictly setting mapper to use by {@link #setMapper} + * <li>By defining JAX-RS <code>Provider</code> that returns {@link ObjectMapper}s. + * <li>By doing none of above, in which case a default mapper instance is + * constructed (and configured if configuration methods are called) + * </ul> + * The last method ("do nothing specific") is often good enough; explicit passing + * of Mapper is simple and explicit; and Provider-based method may make sense + * with Depedency Injection frameworks, or if Mapper has to be configured differently + * for different media types. + *<p> + * Note that the default mapper instance will be automatically created if + * one of explicit configuration methods (like {@link #configure}) + * is called: if so, Provider-based introspection is <b>NOT</b> used, but the + * resulting Mapper is used as configured. + *<p> + * Note: version 1.3 added a sub-class ({@link JacksonJaxbJsonProvider}) which + * is configured by default to use both Jackson and JAXB annotations for configuration + * (base class when used as-is defaults to using just Jackson annotations) + * + * @author Tatu Saloranta + */ +@Provider +@Consumes(MediaType.WILDCARD) // NOTE: required to support "non-standard" JSON variants +@Produces(MediaType.WILDCARD) +public class JacksonJsonProvider + implements + MessageBodyReader<Object>, + MessageBodyWriter<Object>, + Versioned +{ + /** + * Default annotation sets to use, if not explicitly defined during + * construction: only Jackson annotations are used for the base + * class. Sub-classes can use other settings. + */ + public final static Annotations[] BASIC_ANNOTATIONS = { + Annotations.JACKSON + }; + + /** + * 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. + *<p> + * (why ClassKey? since plain old Class has no hashCode() defined, + * lookups are painfully slow) + */ + public final static HashSet<ClassKey> _untouchables = new HashSet<ClassKey>(); + 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(char[].class)); + + /* 28-Jan-2012, tatu: 1.x excluded some additional types; + * but let's relax these a bit: + */ + /* 27-Apr-2012, tatu: Ugh. As per + * [https://github.com/FasterXML/jackson-jaxrs-json-provider/issues/12] + * better revert this back, to make them untouchable again. + */ + _untouchables.add(new ClassKey(String.class)); + _untouchables.add(new ClassKey(byte[].class)); + } + + /** + * These are classes that we never use for reading + * (never try to deserialize instances of these types). + */ + public final static Class<?>[] _unreadableClasses = new Class<?>[] { + InputStream.class, Reader.class + }; + + /** + * These are classes that we never use for writing + * (never try to serialize instances of these types). + */ + public final static Class<?>[] _unwritableClasses = new Class<?>[] { + OutputStream.class, Writer.class, + StreamingOutput.class, Response.class + }; + + /* + /********************************************************** + /* Bit of caching + /********************************************************** + */ + + /** + * Cache for resolved endpoint configurations when reading JSON data + */ + protected final LRUMap<AnnotationBundleKey, EndpointConfig> _readers + = new LRUMap<AnnotationBundleKey, EndpointConfig>(16, 120); + + /** + * Cache for resolved endpoint configurations when writing JSON data + */ + protected final LRUMap<AnnotationBundleKey, EndpointConfig> _writers + = new LRUMap<AnnotationBundleKey, EndpointConfig>(16, 120); + + /* + /********************************************************** + /* General configuration + /********************************************************** + */ + + /** + * Helper object used for encapsulating configuration aspects + * of {@link ObjectMapper} + */ + protected final MapperConfigurator _mapperConfig; + + /** + * Set of types (classes) that provider should ignore for data binding + */ + protected HashSet<ClassKey> _cfgCustomUntouchables; + + /** + * JSONP function name to use for automatic JSONP wrapping, if any; + * if null, no JSONP wrapping is done. + * Note that this is the default value that can be overridden on + * per-endpoint basis. + */ + protected String _jsonpFunctionName; + + /* + /********************************************************** + /* Context configuration + /********************************************************** + */ + + /** + * Injectable context object used to locate configured + * instance of {@link ObjectMapper} to use for actual + * serialization. + */ + @Context + protected Providers _providers; + + /* + /********************************************************** + /* Configuration + /********************************************************** + */ + + /** + * 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 availability can be bit expensive, 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 availability can be bit expensive, 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, BASIC_ANNOTATIONS); + } + + /** + * @param annotationsToUse Annotation set(s) to use for configuring + * data binding + */ + public JacksonJsonProvider(Annotations... annotationsToUse) + { + this(null, annotationsToUse); + } + + public JacksonJsonProvider(ObjectMapper mapper) + { + this(mapper, BASIC_ANNOTATIONS); + } + + /** + * Constructor to use when a custom mapper (usually components + * like serializer/deserializer factories that have been configured) + * is to be used. + * + * @param annotationsToUse Sets of annotations (Jackson, JAXB) that provider should + * support + */ + public JacksonJsonProvider(ObjectMapper mapper, Annotations[] annotationsToUse) + { + _mapperConfig = new MapperConfigurator(mapper, annotationsToUse); + } + + /** + * Method that will return version information stored in and read from jar + * that contains this class. + */ + public Version version() { + return PackageVersion.VERSION; + } + + /* + /********************************************************** + /* Configuring + /********************************************************** + */ + + /** + * Method for defining whether actual detection for existence of + * a deserializer for type should be done when {@link #isReadable} + * is called. + */ + public void checkCanDeserialize(boolean state) { _cfgCheckCanDeserialize = state; } + + /** + * Method for defining whether actual detection for existence of + * a serializer for type should be done when {@link #isWriteable} + * is called. + */ + public void checkCanSerialize(boolean state) { _cfgCheckCanSerialize = state; } + + /** + * Method for configuring which annotation sets to use (including none). + * Annotation sets are defined in order decreasing precedence; that is, + * first one has the priority over following ones. + * + * @param annotationsToUse Ordered list of annotation sets to use; if null, + * default + */ + public void setAnnotationsToUse(Annotations[] annotationsToUse) { + _mapperConfig.setAnnotationsToUse(annotationsToUse); + } + + /** + * Method that can be used to directly define {@link ObjectMapper} to use + * for serialization and deserialization; if null, will use the standard + * provider discovery from context instead. Default setting is null. + */ + public void setMapper(ObjectMapper m) { + _mapperConfig.setMapper(m); + } + + public JacksonJsonProvider configure(DeserializationFeature f, boolean state) { + _mapperConfig.configure(f, state); + return this; + } + + public JacksonJsonProvider configure(SerializationFeature f, boolean state) { + _mapperConfig.configure(f, state); + return this; + } + + public JacksonJsonProvider configure(JsonParser.Feature f, boolean state) { + _mapperConfig.configure(f, state); + return this; + } + + public JacksonJsonProvider configure(JsonGenerator.Feature f, boolean state) { + _mapperConfig.configure(f, state); + return this; + } + + public JacksonJsonProvider enable(DeserializationFeature f, boolean state) { + _mapperConfig.configure(f, true); + return this; + } + + public JacksonJsonProvider enable(SerializationFeature f, boolean state) { + _mapperConfig.configure(f, true); + return this; + } + + public JacksonJsonProvider enable(JsonParser.Feature f, boolean state) { + _mapperConfig.configure(f, true); + return this; + } + + public JacksonJsonProvider enable(JsonGenerator.Feature f, boolean state) { + _mapperConfig.configure(f, true); + return this; + } + + public JacksonJsonProvider disable(DeserializationFeature f, boolean state) { + _mapperConfig.configure(f, false); + return this; + } + + public JacksonJsonProvider disable(SerializationFeature f, boolean state) { + _mapperConfig.configure(f, false); + return this; + } + + public JacksonJsonProvider disable(JsonParser.Feature f, boolean state) { + _mapperConfig.configure(f, false); + return this; + } + + public JacksonJsonProvider disable(JsonGenerator.Feature f, boolean state) { + _mapperConfig.configure(f, false); + return this; + } + + /** + * Method for marking specified type as "untouchable", meaning that provider + * will not try to read or write values of this type (or its subtypes). + * + * @param type Type to consider untouchable; can be any kind of class, + * including abstract class or interface. No instance of this type + * (including subtypes, i.e. types assignable to this type) will + * be read or written by provider + */ + public void addUntouchable(Class<?> type) + { + if (_cfgCustomUntouchables == null) { + _cfgCustomUntouchables = new HashSet<ClassKey>(); + } + _cfgCustomUntouchables.add(new ClassKey(type)); + } + + public void setJSONPFunctionName(String fname) { + this._jsonpFunctionName = fname; + } + + /* + /********************************************************** + /* MessageBodyReader impl + /********************************************************** + */ + + /** + * Method that JAX-RS container calls to try to check whether + * values of given type (and media type) can be deserialized by + * this provider. + * Implementation will first check that expected media type is + * a JSON type (via call to {@link #isJsonType}; then verify + * that type is not one of "untouchable" types (types we will never + * automatically handle), and finally that there is a deserializer + * for type (iff {@link #checkCanDeserialize} has been called with + * true argument -- otherwise assumption is there will be a handler) + */ + 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; + } + // and there are some other abstract/interface types to exclude too: + for (Class<?> cls : _unreadableClasses) { + if (cls.isAssignableFrom(type)) { + return false; + } + } + // as well as possible custom exclusions + if (_containedIn(type, _cfgCustomUntouchables)) { + return false; + } + + // Finally: if we really want to verify that we can serialize, we'll check: + if (_cfgCheckCanSerialize) { + ObjectMapper mapper = locateMapper(type, mediaType); + if (!mapper.canDeserialize(mapper.constructType(type))) { + return false; + } + } + return true; + } + + /** + * Method that JAX-RS container calls to deserialize given value. + */ + public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String,String> httpHeaders, InputStream entityStream) + throws IOException + { + + AnnotationBundleKey key = new AnnotationBundleKey(annotations); + EndpointConfig endpoint; + synchronized (_readers) { + endpoint = _readers.get(key); + } + // not yet resolved (or not cached any more)? Resolve! + if (endpoint == null) { + ObjectMapper mapper = locateMapper(type, mediaType); + endpoint = EndpointConfig.forReading(mapper, annotations); + // and cache for future reuse + synchronized (_readers) { + _readers.put(key.immutableKey(), endpoint); + } + } + ObjectReader reader = endpoint.getReader(); + + JsonParser jp = reader.getFactory().createParser(entityStream); + if (jp.nextToken() == null) { + return null; + } + return reader.withType(genericType).readValue(jp); + } + + /* + /********************************************************** + /* MessageBodyWriter impl + /********************************************************** + */ + + /** + * Method that JAX-RS container calls to try to figure out + * serialized length of given value. Since computation of + * this length is about as expensive as serialization itself, + * implementation will return -1 to denote "not known", so + * that container will determine length from actual serialized + * output (if needed). + */ + 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; + } + + /** + * Method that JAX-RS container calls to try to check whether + * given value (of specified type) can be serialized by + * this provider. + * Implementation will first check that expected media type is + * a JSON type (via call to {@link #isJsonType}; then verify + * that type is not one of "untouchable" types (types we will never + * automatically handle), and finally that there is a serializer + * for type (iff {@link #checkCanSerialize} has been called with + * true argument -- otherwise assumption is there will be a handler) + */ + 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; + } + } + // and finally, may have additional custom types to exclude + if (_containedIn(type, _cfgCustomUntouchables)) { + return false; + } + + // Also: if we really want to verify that we can deserialize, we'll check: + if (_cfgCheckCanSerialize) { + if (!locateMapper(type, mediaType).canSerialize(type)) { + return false; + } + } + return true; + } + + /** + * Method that JAX-RS container calls to serialize given value. + */ + public void writeTo(Object value, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap<String,Object> httpHeaders, OutputStream entityStream) + throws IOException + { + AnnotationBundleKey key = new AnnotationBundleKey(annotations); + EndpointConfig endpoint; + synchronized (_writers) { + endpoint = _writers.get(key); + } + // not yet resolved (or not cached any more)? Resolve! + if (endpoint == null) { + ObjectMapper mapper = locateMapper(type, mediaType); + endpoint = EndpointConfig.forWriting(mapper, annotations, + this._jsonpFunctionName); + // and cache for future reuse + synchronized (_writers) { + _writers.put(key.immutableKey(), endpoint); + } + } + + ObjectWriter writer = endpoint.getWriter(); + + /* 27-Feb-2009, tatu: Where can we find desired encoding? Within + * HTTP headers? + */ + JsonEncoding enc = findEncoding(mediaType, httpHeaders); + JsonGenerator jg = writer.getFactory().createGenerator(entityStream, enc); + + // Want indentation? + if (writer.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + jg.useDefaultPrettyPrinter(); + } + // 04-Mar-2010, tatu: How about type we were given? (if any) + JavaType rootType = null; + + if (genericType != null && value != null) { + /* 10-Jan-2011, tatu: as per [JACKSON-456], it's not safe to just force root + * type since it prevents polymorphic type serialization. Since we really + * just need this for generics, let's only use generic type if it's truly + * generic. + */ + if (genericType.getClass() != Class.class) { // generic types are other impls of 'java.lang.reflect.Type' + /* This is still not exactly right; should root type be further + * specialized with 'value.getClass()'? Let's see how well this works before + * trying to come up with more complete solution. + */ + rootType = writer.getTypeFactory().constructType(genericType); + /* 26-Feb-2011, tatu: To help with [JACKSON-518], we better recognize cases where + * type degenerates back into "Object.class" (as is the case with plain TypeVariable, + * for example), and not use that. + */ + if (rootType.getRawClass() == Object.class) { + rootType = null; + } + } + } + // Most of the configuration now handled through EndpointConfig, ObjectWriter + // but we may need to force root type: + if (rootType != null) { + writer = writer.withType(rootType); + } + // and finally, JSONP wrapping, if any: + value = endpoint.applyJSONP(value); + + writer.writeValue(jg, value); + } + + /** + * Helper method to use for determining desired output encoding. + * For now, will always just use UTF-8... + */ + protected JsonEncoding findEncoding(MediaType mediaType, MultivaluedMap<String,Object> httpHeaders) + { + return JsonEncoding.UTF8; + } + + /* + /********************************************************** + /* Public helper methods + /********************************************************** + */ + + /** + * Helper method used to check whether given media type + * is JSON type or sub type. + * Current implementation essentially checks to see whether + * {@link MediaType#getSubtype} returns "json" or something + * ending with "+json". + */ + 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".equalsIgnoreCase(subtype) || subtype.endsWith("+json"); + } + /* Not sure if this can happen; but it seems reasonable + * that we can at least produce json without media type? + */ + return true; + } + + /** + * Method called to locate {@link ObjectMapper} to use for serialization + * and deserialization. If an instance has been explicitly defined by + * {@link #setMapper} (or non-null instance passed in constructor), that + * will be used. + * If not, will try to locate it using standard JAX-RS + * {@link ContextResolver} mechanism, if it has been properly configured + * to access it (by JAX-RS runtime). + * Finally, if no mapper is found, will return a default unconfigured + * {@link ObjectMapper} instance (one constructed with default constructor + * and not modified in any way) + * + * @param type Class of object being serialized or deserialized; + * not checked at this point, since it is assumed that unprocessable + * classes have been already weeded out, + * but will be passed to {@link ContextResolver} as is. + * @param mediaType Declared media type for the instance to process: + * not used by this method, + * but will be passed to {@link ContextResolver} as is. + */ + public ObjectMapper locateMapper(Class<?> type, MediaType mediaType) + { + // First: were we configured with a specific instance? + ObjectMapper m = _mapperConfig.getConfiguredMapper(); + if (m == null) { + // If not, maybe we can get one configured via context? + if (_providers != null) { + ContextResolver<ObjectMapper> resolver = _providers.getContextResolver(ObjectMapper.class, mediaType); + /* Above should work as is, but due to this bug + * [https://jersey.dev.java.net/issues/show_bug.cgi?id=288] + * in Jersey, it doesn't. But this works until resolution of + * the issue: + */ + if (resolver == null) { + resolver = _providers.getContextResolver(ObjectMapper.class, null); + } + if (resolver != null) { + m = resolver.getContext(type); + } + } + if (m == null) { + // If not, let's get the fallback default instance + m = _mapperConfig.getDefaultMapper(); + } + } + return m; + } + + /* + /********************************************************** + /* Private/sub-class helper methods + /********************************************************** + */ + + protected static boolean _containedIn(Class<?> mainType, HashSet<ClassKey> set) + { + if (set != null) { + ClassKey key = new ClassKey(mainType); + // First: type itself? + if (set.contains(key)) return true; + // Then supertypes (note: will not contain Object.class) + for (Class<?> cls : findSuperTypes(mainType, null)) { + key.reset(cls); + if (set.contains(key)) return true; + } + } + return false; + } + + private static List<Class<?>> findSuperTypes(Class<?> cls, Class<?> endBefore) + { + return findSuperTypes(cls, endBefore, new ArrayList<Class<?>>(8)); + } + + private static List<Class<?>> findSuperTypes(Class<?> cls, Class<?> endBefore, List<Class<?>> result) + { + _addSuperTypes(cls, endBefore, result, false); + return result; + } + + private static void _addSuperTypes(Class<?> cls, Class<?> endBefore, Collection<Class<?>> result, boolean addClassItself) + { + if (cls == endBefore || cls == null || cls == Object.class) { + return; + } + if (addClassItself) { + if (result.contains(cls)) { // already added, no need to check supers + return; + } + result.add(cls); + } + for (Class<?> intCls : cls.getInterfaces()) { + _addSuperTypes(intCls, endBefore, result, true); + } + _addSuperTypes(cls.getSuperclass(), endBefore, result, true); + } + +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonMappingExceptionMapper.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonMappingExceptionMapper.java new file mode 100644 index 0000000..7d07679 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonMappingExceptionMapper.java @@ -0,0 +1,18 @@ +package com.fasterxml.jackson.jaxrs.json; + +import com.fasterxml.jackson.databind.JsonMappingException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Implementation if {@link ExceptionMapper} to send down a "400 Bad Request" + * response in the event that unmappable JSON is received. + */ +@Provider +public class JsonMappingExceptionMapper implements ExceptionMapper<JsonMappingException> { + public Response toResponse(JsonMappingException exception) { + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonParseExceptionMapper.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonParseExceptionMapper.java new file mode 100644 index 0000000..429caf1 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/JsonParseExceptionMapper.java @@ -0,0 +1,18 @@ +package com.fasterxml.jackson.jaxrs.json; + +import com.fasterxml.jackson.core.JsonParseException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Implementation of {@link ExceptionMapper} to send down a "400 Bad Request" + * in the event unparsable JSON is received. + */ +@Provider +public class JsonParseExceptionMapper implements ExceptionMapper<JsonParseException> { + public Response toResponse(JsonParseException exception) { + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/PackageVersion.java.in b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/PackageVersion.java.in new file mode 100644 index 0000000..7860aa1 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/EndpointConfig.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/EndpointConfig.java new file mode 100644 index 0000000..ea7d099 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/EndpointConfig.java @@ -0,0 +1,214 @@ +package com.fasterxml.jackson.jaxrs.json.annotation; + +import java.lang.annotation.Annotation; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.util.JSONPObject; +import com.fasterxml.jackson.databind.util.JSONWrappedObject; + +/** + * Container class for figuring out annotation-based configuration + * for JAX-RS end points. + */ +public class EndpointConfig +{ + // // General configuration + + protected Class<?> _activeView; + + protected String _rootName; + + // // Deserialization-only config + + protected DeserializationFeature[] _deserEnable; + protected DeserializationFeature[] _deserDisable; + + protected ObjectReader _reader; + + // // Serialization-only config + + protected JSONP.Def _jsonp; + + protected SerializationFeature[] _serEnable; + protected SerializationFeature[] _serDisable; + + protected ObjectWriter _writer; + + /* + /********************************************************** + /* Construction + /********************************************************** + */ + + protected EndpointConfig() { } + + public static EndpointConfig forReading(ObjectMapper mapper, Annotation[] annotations) + { + return new EndpointConfig() + .add(annotations, false) + .initReader(mapper); + } + + public static EndpointConfig forWriting(ObjectMapper mapper, Annotation[] annotations, + String defaultJsonpMethod) + { + EndpointConfig config = new EndpointConfig(); + if (defaultJsonpMethod != null) { + config._jsonp = new JSONP.Def(defaultJsonpMethod); + } + return config + .add(annotations, true) + .initWriter(mapper) + ; + } + + protected EndpointConfig add(Annotation[] annotations, boolean forWriting) + { + if (annotations != null) { + for (Annotation annotation : annotations) { + Class<?> type = annotation.annotationType(); + if (type == JSONP.class) { + if (forWriting) { + _jsonp = new JSONP.Def((JSONP) annotation); + } + } else if (type == JsonView.class) { + // Can only use one view; but if multiple defined, use first (no exception) + Class<?>[] views = ((JsonView) annotation).value(); + _activeView = (views.length > 0) ? views[0] : null; + } else if (type == JsonRootName.class) { + _rootName = ((JsonRootName) annotation).value(); + } else if (type == JacksonFeatures.class) { + JacksonFeatures feats = (JacksonFeatures) annotation; + if (forWriting) { + _serEnable = nullIfEmpty(feats.serializationEnable()); + _serDisable = nullIfEmpty(feats.serializationDisable()); + } else { + _deserEnable = nullIfEmpty(feats.deserializationEnable()); + _deserDisable = nullIfEmpty(feats.deserializationDisable()); + } + } else if (type == JacksonAnnotationsInside.class) { + // skip; processed below (in parent), so encountering here is of no use + } else { + // For all unrecognized types, check meta-annotation(s) to see if they are bundles + JacksonAnnotationsInside inside = type.getAnnotation(JacksonAnnotationsInside.class); + if (inside != null) { + add(type.getAnnotations(), forWriting); + } + } + } + } + return this; + } + + protected EndpointConfig initReader(ObjectMapper mapper) + { + // first common config + if (_activeView != null) { + _reader = mapper.readerWithView(_activeView); + } else { + _reader = mapper.reader(); + } + + if (_rootName != null) { + _reader = _reader.withRootName(_rootName); + } + // Then deser features + if (_deserEnable != null) { + _reader = _reader.withFeatures(_deserEnable); + } + if (_deserDisable != null) { + _reader = _reader.withoutFeatures(_deserDisable); + } + /* Important: we are NOT to close the underlying stream after + * mapping, so we need to instruct parser: + */ + _reader.getFactory().disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + + return this; + } + + protected EndpointConfig initWriter(ObjectMapper mapper) + { + // first common config + if (_activeView != null) { + _writer = mapper.writerWithView(_activeView); + } else { + _writer = mapper.writer(); + } + if (_rootName != null) { + _writer = _writer.withRootName(_rootName); + } + // Then features + if (_serEnable != null) { + _writer = _writer.withFeatures(_serEnable); + } + if (_serDisable != null) { + _writer = _writer.withoutFeatures(_serDisable); + } + // then others + + // Finally: couple of features we always set + + /* Important: we are NOT to close the underlying stream after + * mapping, so we need to instruct parser: + */ + _writer.getFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + + return this; + } + + /* + /********************************************************** + /* Accessors + /********************************************************** + */ + + public ObjectReader getReader() { + if (_reader == null) { // sanity check, should never happen + throw new IllegalStateException(); + } + return _reader; + } + + public ObjectWriter getWriter() { + if (_writer == null) { // sanity check, should never happen + throw new IllegalStateException(); + } + return _writer; + } + + /** + * Method that will add JSONP wrapper object, if and as + * configured by collected annotations. + */ + public Object applyJSONP(Object value) + { + if (_jsonp != null) { + // full prefix+suffix? + if (_jsonp.prefix != null || _jsonp.suffix != null) { + return new JSONWrappedObject(_jsonp.prefix, _jsonp.suffix, value); + } + if (_jsonp.method != null) { + return new JSONPObject(_jsonp.method, value); + } + } + return value; + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private static <T> T[] nullIfEmpty(T[] arg) { + if (arg == null || arg.length == 0) { + return null; + } + return arg; + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JSONP.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JSONP.java new file mode 100644 index 0000000..620292c --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JSONP.java @@ -0,0 +1,94 @@ +package com.fasterxml.jackson.jaxrs.json.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* Note: applicable to annotations to allow bundling (if support added + * to JAX-RS bundle itself), as well as methods to indicate that return + * type is to be wrapped. + * Other types are not allowed, since there is no current usage for those; + * input can't be wrapped (so no need for parameters); fields are not + * exposed through JAX-RS; and we do not allow 'default wrapping' for + * types. + *<p> + * Note on properties: if either {@link #prefix()} or {@link #suffix()} + * is non-empty, they are used as literal prefix and suffix to use. + * Otherwise {@link #value()} is used as the function name, followed + * by opening parenthesis, value, and closing parenthesis. + *<p> + * Example usage: + *<pre> + * class Wrapper { + * @JSONP("myFunc") public int value = 3; + * } + *</pre> + * would serialize as: + *<pre> + * myFunc({"value":3}) + *<pre> + * whereas + *</pre> + *<pre> + * class Wrapper { + * @JSONP(prefix="call(", suffix=")+3") public int value = 1; + * } + *</pre> + * would serialize as: + *<pre> + * call({"value":1})+3 + *<pre> + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) [email protected] +public @interface JSONP +{ + /** + * Method used for JSONP, unless {@link #prefix()} or + * {@link #suffix()} return non-empty Strings. + */ + public String value() default ""; + + /** + * Prefix String used for JSONP if not empty: will be included + * verbatim before JSON value. + */ + public String prefix() default ""; + + /** + * Suffix String used for JSONP if not empty: will be included + * verbatim after JSON value. + */ + public String suffix() default ""; + + /** + * Helper class for encapsulating information from {@link JSONP} + * annotation instance. + */ + public static class Def { + public final String method; + public final String prefix; + public final String suffix; + + public Def(String m) { + method = m; + prefix = null; + suffix = null; + } + + public Def(JSONP json) { + method = emptyAsNull(json.value()); + prefix = emptyAsNull(json.prefix()); + suffix = emptyAsNull(json.suffix()); + } + + private final static String emptyAsNull(String str) { + if (str == null || str.length() == 0) { + return null; + } + return str; + } + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JacksonFeatures.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JacksonFeatures.java new file mode 100644 index 0000000..c5d0c9c --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/JacksonFeatures.java @@ -0,0 +1,39 @@ +package com.fasterxml.jackson.jaxrs.json.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Annotation that can be used enable and/or disable various + * features for <code>ObjectReader</code>s and <code>ObjectWriter</code>s. + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) [email protected] +public @interface JacksonFeatures +{ + /** + * Deserialization features to enable. + */ + public DeserializationFeature[] deserializationEnable() default { }; + + /** + * Deserialization features to disable. + */ + public DeserializationFeature[] deserializationDisable() default { }; + + /** + * Serialization features to enable. + */ + public SerializationFeature[] serializationEnable() default { }; + + /** + * Serialization features to disable. + */ + public SerializationFeature[] serializationDisable() default { }; +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/package-info.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/package-info.java new file mode 100644 index 0000000..62461ef --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/annotation/package-info.java @@ -0,0 +1,5 @@ +/** + * Package that contains utility classes and methods for + * the JAX-RS JSON provider module. + */ +package com.fasterxml.jackson.jaxrs.json.annotation; diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/cfg/MapperConfigurator.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/cfg/MapperConfigurator.java new file mode 100644 index 0000000..f23dd2a --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/cfg/MapperConfigurator.java @@ -0,0 +1,180 @@ +package com.fasterxml.jackson.jaxrs.json.cfg; + +import java.util.*; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.jaxrs.json.Annotations; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; + +/** + * Helper class used to encapsulate details of configuring an + * {@link ObjectMapper} instance to be used for data binding, as + * well as accessing it. + */ +public class MapperConfigurator +{ + /** + * Mapper provider was constructed with if any, or that was constructed + * due to a call to explicitly configure mapper. + * If defined (explicitly or implicitly) it will be used, instead + * of using provider-based lookup. + */ + protected ObjectMapper _mapper; + + /** + * If no mapper was specified when constructed, and no configuration + * calls are made, a default mapper is constructed. The difference + * between default mapper and regular one is that default mapper + * is only used if no mapper is found via provider lookup. + */ + protected ObjectMapper _defaultMapper; + + /** + * Annotations set to use by default; overridden by explicit call + * to {@link #setAnnotationsToUse} + */ + protected Annotations[] _defaultAnnotationsToUse; + + /** + * To support optional dependency to Jackson JAXB annotations module + * (needed iff JAXB annotations are used for configuration) + */ + protected Class<? extends AnnotationIntrospector> _jaxbIntrospectorClass; + + /* + /********************************************************** + /* Construction + /********************************************************** + */ + + public MapperConfigurator(ObjectMapper mapper, Annotations[] defAnnotations) + { + _mapper = mapper; + _defaultAnnotationsToUse = defAnnotations; + } + + /** + * Method that locates, configures and returns {@link ObjectMapper} to use + */ + public synchronized ObjectMapper getConfiguredMapper() { + /* important: should NOT call mapper(); needs to return null + * if no instance has been passed or constructed + */ + return _mapper; + } + + public synchronized ObjectMapper getDefaultMapper() { + if (_defaultMapper == null) { + _defaultMapper = new ObjectMapper(); + _setAnnotations(_defaultMapper, _defaultAnnotationsToUse); + } + return _defaultMapper; + } + + /* + *********************************************************** + * Configuration methods + *********************************************************** + */ + + public synchronized void setMapper(ObjectMapper m) { + _mapper = m; + } + + public synchronized void setAnnotationsToUse(Annotations[] annotationsToUse) { + _setAnnotations(mapper(), annotationsToUse); + } + + public synchronized void configure(DeserializationFeature f, boolean state) { + mapper().configure(f, state); + } + + public synchronized void configure(SerializationFeature f, boolean state) { + mapper().configure(f, state); + } + + public synchronized void configure(JsonParser.Feature f, boolean state) { + mapper().configure(f, state); + } + + public synchronized void configure(JsonGenerator.Feature f, boolean state) { + mapper().configure(f, state); + } + + /* + *********************************************************** + * Internal methods + *********************************************************** + */ + + /** + * Helper method that will ensure that there is a configurable non-default + * mapper (constructing an instance if one didn't yet exit), and return + * that mapper. + */ + protected ObjectMapper mapper() + { + if (_mapper == null) { + _mapper = new ObjectMapper(); + _setAnnotations(_mapper, _defaultAnnotationsToUse); + } + return _mapper; + } + + protected void _setAnnotations(ObjectMapper mapper, Annotations[] annotationsToUse) + { + AnnotationIntrospector intr; + if (annotationsToUse == null || annotationsToUse.length == 0) { + intr = AnnotationIntrospector.nopInstance(); + } else { + intr = _resolveIntrospectors(annotationsToUse); + } + mapper.setAnnotationIntrospector(intr); + } + + + protected AnnotationIntrospector _resolveIntrospectors(Annotations[] annotationsToUse) + { + // Let's ensure there are no dups there first, filter out nulls + ArrayList<AnnotationIntrospector> intr = new ArrayList<AnnotationIntrospector>(); + for (Annotations a : annotationsToUse) { + if (a != null) { + intr.add(_resolveIntrospector(a)); + } + } + int count = intr.size(); + if (count == 0) { + return AnnotationIntrospector.nopInstance(); + } + AnnotationIntrospector curr = intr.get(0); + for (int i = 1, len = intr.size(); i < len; ++i) { + curr = AnnotationIntrospector.pair(curr, intr.get(i)); + } + return curr; + } + + protected AnnotationIntrospector _resolveIntrospector(Annotations ann) + { + switch (ann) { + case JACKSON: + return new JacksonAnnotationIntrospector(); + case JAXB: + /* For this, need to use indirection just so that error occurs + * when we get here, and not when this class is being loaded + */ + try { + if (_jaxbIntrospectorClass == null) { + _jaxbIntrospectorClass = JaxbAnnotationIntrospector.class; + } + return _jaxbIntrospectorClass.newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate JaxbAnnotationIntrospector: "+e.getMessage(), e); + } + default: + throw new IllegalStateException(); + } + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/package-info.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/package-info.java new file mode 100644 index 0000000..72248c3 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/package-info.java @@ -0,0 +1,21 @@ +/** + * Jackson-based JAX-RS provider that can automatically + * serialize and deserialize resources for + * JSON content type (MediaType). + *<p> + * Also continues supporting functionality, such as + * exception mappers that can simplify handling of + * error conditions. + *<p> + * There are two default provider classes: + *<ul> + * <li>{@link com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider} is the basic + * provider configured to use Jackson annotations + * </li> + * <li>{@link com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider} is extension + * of the basic provider, configured to additionally use JAXB annotations, + * in addition to (or in addition of, if so configured) Jackson annotations. + * </li> + * </ul> + */ +package com.fasterxml.jackson.jaxrs.json; diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/AnnotationBundleKey.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/AnnotationBundleKey.java new file mode 100644 index 0000000..e06e01f --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/AnnotationBundleKey.java @@ -0,0 +1,110 @@ +package com.fasterxml.jackson.jaxrs.json.util; + +import java.lang.annotation.Annotation; + +/** + * Helper class used to allow efficient caching of information, + * given a sequence of Annotations. + * This is mostly used for reusing introspected information on + * JAX-RS end points. + */ +public final class AnnotationBundleKey +{ + private final static Annotation[] NO_ANNOTATIONS = new Annotation[0]; + + private final Annotation[] _annotations; + + private final boolean _annotationsCopied; + + private final int _hashCode; + + /* + /********************************************************** + /* Construction + /********************************************************** + */ + + public AnnotationBundleKey(Annotation[] annotations) + { + if (annotations == null || annotations.length == 0) { + annotations = NO_ANNOTATIONS; + _annotationsCopied = true; + _hashCode = -1; + } else { + _annotationsCopied = false; + _hashCode = calcHash(annotations); + } + _annotations = annotations; + } + + private AnnotationBundleKey(Annotation[] annotations, int hashCode) + { + _annotations = annotations; + _annotationsCopied = true; + _hashCode = hashCode; + } + + private final static int calcHash(Annotation[] annotations) + { + /* hmmh. Can't just base on Annotation type; chances are that Annotation + * instances use identity hash, which has to do. + */ + final int len = annotations.length; + int hash = len; + for (int i = 0; i < len; ++i) { + hash = (hash * 31) + annotations[i].hashCode(); + } + return hash; + } + + /** + * Method called to create a safe immutable copy of the key; used when + * adding entry with this key -- lookups are ok without calling the method. + */ + public AnnotationBundleKey immutableKey() { + if (_annotationsCopied) { + return this; + } + int len = _annotations.length; + Annotation[] newAnnotations = new Annotation[len]; + System.arraycopy(_annotations, 0, newAnnotations, 0, len); + return new AnnotationBundleKey(newAnnotations, _hashCode); + } + + /* + /********************************************************** + /* Overridden methods + /********************************************************** + */ + + @Override + public String toString() { + return "[Annotations: "+_annotations.length+", hash 0x"+Integer.toHexString(_hashCode) + +", copied: "+_annotationsCopied+"]"; + } + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != getClass()) return false; + AnnotationBundleKey other = (AnnotationBundleKey) o; + if (other._hashCode != _hashCode) return false; + return _equals(other._annotations); + } + + private final boolean _equals(Annotation[] otherAnn) + { + final int len = _annotations.length; + if (otherAnn.length != len) { + return false; + } + for (int i = 0; i < len; ++i) { + if (_annotations[i] != otherAnn[i]) { + return false; + } + } + return true; + } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/ClassKey.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/ClassKey.java new file mode 100644 index 0000000..7251774 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/ClassKey.java @@ -0,0 +1,92 @@ +package com.fasterxml.jackson.jaxrs.json.util; + +/** + * Efficient key class, used instead of using <code>Class</code>. + * The reason for having a separate key class instead of + * directly using {@link Class} as key is mostly + * to allow for redefining <code>hashCode</code> method -- + * for some strange reason, {@link Class} does not + * redefine {@link Object#hashCode} and thus uses identity + * hash, which is pretty slow. This makes key access using + * {@link Class} unnecessarily slow. + *<p> + * Note: since class is not strictly immutable, caller must + * know what it is doing, if changing field values. + *<p> + * NOTE: cut'n pasted from 'databind' package for 2.0, to reduce + * tight coupling + */ +public final class ClassKey + implements Comparable<ClassKey> +{ + private String _className; + + private Class<?> _class; + + /** + * Let's cache hash code straight away, since we are + * almost certain to need it. + */ + private int _hashCode; + + public ClassKey() + { + _class = null; + _className = null; + _hashCode = 0; + } + + public ClassKey(Class<?> clz) + { + _class = clz; + _className = clz.getName(); + _hashCode = _className.hashCode(); + } + + public void reset(Class<?> clz) + { + _class = clz; + _className = clz.getName(); + _hashCode = _className.hashCode(); + } + + /* + /********************************************************** + /* Comparable + /********************************************************** + */ + + // Just need to sort by name, ok to collide (unless used in TreeMap/Set!) + //@Override + public int compareTo(ClassKey other) { + return _className.compareTo(other._className); + } + + /* + /********************************************************** + /* Standard methods + /********************************************************** + */ + + @Override + public boolean equals(Object o) + { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != getClass()) return false; + ClassKey other = (ClassKey) o; + + /* Is it possible to have different Class object for same name + class loader combo? + * Let's assume answer is no: if this is wrong, will need to uncomment following functionality + */ + /* + return (other._className.equals(_className)) + && (other._class.getClassLoader() == _class.getClassLoader()); + */ + return other._class == _class; + } + + @Override public int hashCode() { return _hashCode; } + + @Override public String toString() { return _className; } +} diff --git a/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/LRUMap.java b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/LRUMap.java new file mode 100644 index 0000000..fa4a956 --- /dev/null +++ b/json/src/main/java/com/fasterxml/jackson/jaxrs/json/util/LRUMap.java @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.jaxrs.json.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Helper for simple bounded LRU maps used for reusing lookup values. + */ +@SuppressWarnings("serial") +public class LRUMap<K,V> extends LinkedHashMap<K,V> +{ + protected final int _maxEntries; + + public LRUMap(int initialEntries, int maxEntries) + { + super(initialEntries, 0.8f, true); + _maxEntries = maxEntries; + } + + @Override + protected boolean removeEldestEntry(Map.Entry<K,V> eldest) + { + return size() > _maxEntries; + } + +} diff --git a/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader b/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader new file mode 100644 index 0000000..4abbe18 --- /dev/null +++ b/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyReader @@ -0,0 +1 @@ +com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider diff --git a/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter b/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter new file mode 100644 index 0000000..4abbe18 --- /dev/null +++ b/json/src/main/resources/META-INF/services/javax.ws.rs.ext.MessageBodyWriter @@ -0,0 +1 @@ +com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/JaxrsTestBase.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/JaxrsTestBase.java new file mode 100644 index 0000000..34d56ab --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/JaxrsTestBase.java @@ -0,0 +1,92 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Assert; + +import com.fasterxml.jackson.core.*; + +public abstract class JaxrsTestBase + extends junit.framework.TestCase +{ + + /* + /********************************************************** + /* Additional assertion methods + /********************************************************** + */ + + protected void assertToken(JsonToken expToken, JsonToken actToken) + { + if (actToken != expToken) { + fail("Expected token "+expToken+", current token "+actToken); + } + } + + protected void assertToken(JsonToken expToken, JsonParser jp) + { + assertToken(expToken, jp.getCurrentToken()); + } + + protected void assertType(Object ob, Class<?> expType) + { + if (ob == null) { + fail("Expected an object of type "+expType.getName()+", got null"); + } + Class<?> cls = ob.getClass(); + if (!expType.isAssignableFrom(cls)) { + fail("Expected type "+expType.getName()+", got "+cls.getName()); + } + } + + protected void verifyException(Throwable e, String... matches) + { + String msg = e.getMessage(); + String lmsg = (msg == null) ? "" : msg.toLowerCase(); + for (String match : matches) { + String lmatch = match.toLowerCase(); + if (lmsg.indexOf(lmatch) >= 0) { + return; + } + } + fail("Expected an exception with one of substrings ("+Arrays.asList(matches)+"): got one with message \""+msg+"\""); + } + + protected void _verifyBytes(byte[] actBytes, byte... expBytes) + { + Assert.assertArrayEquals(expBytes, actBytes); + } + + /** + * Method that gets textual contents of the current token using + * available methods, and ensures results are consistent, before + * returning them + */ + protected String getAndVerifyText(JsonParser jp) + throws IOException, JsonParseException + { + // Ok, let's verify other accessors + int actLen = jp.getTextLength(); + char[] ch = jp.getTextCharacters(); + String str2 = new String(ch, jp.getTextOffset(), actLen); + String str = jp.getText(); + + if (str.length() != actLen) { + fail("Internal problem (jp.token == "+jp.getCurrentToken()+"): jp.getText().length() ['"+str+"'] == "+str.length()+"; jp.getTextLength() == "+actLen); + } + assertEquals("String access via getText(), getTextXxx() must be the same", str, str2); + + return str; + } + + /* + /********************************************************** + /* Other helper methods + /********************************************************** + */ + + public String quote(String str) { + return '"'+str+'"'; + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanDeserialize.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanDeserialize.java new file mode 100644 index 0000000..dafb45c --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanDeserialize.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +/** + * Unit test to check [JACKSON-540] + */ +public class TestCanDeserialize extends JaxrsTestBase { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testCanSerialize() throws IOException { + Map<String, Object> object = new LinkedHashMap<String, Object>(); + JacksonJsonProvider prov = new JacksonJsonProvider(); + + String json = "{\"foo\":\"bar\"}"; + InputStream stream = new ByteArrayInputStream(json.getBytes()); + + object = (Map) prov.readFrom(Object.class, object.getClass(), new Annotation[0], + MediaType.APPLICATION_JSON_TYPE, null, stream); + + assertEquals("bar", object.get("foo")); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testCanSerializeEmpty() throws IOException { + Map<String, Object> object = new LinkedHashMap<String, Object>(); + JacksonJsonProvider prov = new JacksonJsonProvider(); + + InputStream stream = new ByteArrayInputStream(new byte[0]); + + object = (Map) prov.readFrom(Object.class, object.getClass(), new Annotation[0], + MediaType.APPLICATION_JSON_TYPE, null, stream); + + assertNull(object); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanSerialize.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanSerialize.java new file mode 100644 index 0000000..7d57a4b --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestCanSerialize.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.*; +import java.util.*; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Unit test to check [JACKSON-540] + */ +public class TestCanSerialize extends JaxrsTestBase +{ + static class Simple { + protected List<String> list; + + public List<String> getList( ) { return list; } + public void setList(List<String> l) { list = l; } + } + + public void testCanSerialize() throws IOException + { + ObjectMapper mapper = new ObjectMapper(); + mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY); + + // construct test object + List<String> l = new ArrayList<String>(); + l.add("foo"); + l.add("bar"); + + Simple s = new Simple(); + s.setList(l); + + // this is fine: + boolean can = mapper.canSerialize(Simple.class); + assertTrue(can); + + // but with problem of [JACKSON-540], we get nasty surprise here... + String json = mapper.writeValueAsString(s); + + Simple result = mapper.readValue(json, Simple.class); + assertNotNull(result.list); + assertEquals(2, result.list.size()); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJacksonFeatures.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJacksonFeatures.java new file mode 100644 index 0000000..9c3dc12 --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJacksonFeatures.java @@ -0,0 +1,111 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.*; +import java.lang.annotation.*; +import java.lang.reflect.Method; + +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializationFeature; + +import com.fasterxml.jackson.jaxrs.json.annotation.JacksonFeatures; + +/** + * Tests for [Issue-2], Addition of {@link JacksonFeatures}. + */ +public class TestJacksonFeatures extends JaxrsTestBase +{ + static class Bean { + public int a = 3; + } + + @JacksonFeatures(serializationEnable={ SerializationFeature.WRAP_ROOT_VALUE }) + public void writeConfig() { } + + @JacksonFeatures(deserializationDisable={ DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES }) + public void readConfig() { } + + // Also, let's check that we can bundle annotations + @JacksonAnnotationsInside + @JacksonFeatures(serializationEnable={ SerializationFeature.WRAP_ROOT_VALUE }) + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface FeatureBundle { } + + @FeatureBundle // should work as if all annotations from FeatureBundle were directly added + public void writeConfig2() { } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + // [Issue-2], serialization + public void testWriteConfigs() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bean bean = new Bean(); + Method m = getClass().getDeclaredMethod("writeConfig"); + JacksonFeatures feats = m.getAnnotation(JacksonFeatures.class); + assertNotNull(feats); // just a sanity check + + // when wrapping enabled, we get: + prov.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[] { feats }, + MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("{\"Bean\":{\"a\":3}}", out.toString("UTF-8")); + + // but without, not: + out.reset(); + prov.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[] { }, + MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("{\"a\":3}", out.toString("UTF-8")); + } + + public void testWriteConfigsViaBundle() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bean bean = new Bean(); + Method m = getClass().getDeclaredMethod("writeConfig2"); + // should still enable root-wrapping + prov.writeTo(bean, bean.getClass(), bean.getClass(), m.getAnnotations(), + MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("{\"Bean\":{\"a\":3}}", out.toString("UTF-8")); + } + + // [Issue-2], deserialization + public void testReadConfigs() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + Method m = getClass().getDeclaredMethod("readConfig"); + JacksonFeatures feats = m.getAnnotation(JacksonFeatures.class); + assertNotNull(feats); // just a sanity check + + // ok: here let's verify that we can disable exception throwing unrecognized things + @SuppressWarnings("unchecked") + Class<Object> raw = (Class<Object>)(Class<?>)Bean.class; + Object ob = prov.readFrom(raw, raw, + new Annotation[] { feats }, + MediaType.APPLICATION_JSON_TYPE, null, + new ByteArrayInputStream("{ \"foobar\" : 3 }".getBytes("UTF-8"))); + assertNotNull(ob); + + // but without setting, get the exception + try { + prov.readFrom(raw, raw, + new Annotation[] { }, + MediaType.APPLICATION_JSON_TYPE, null, + new ByteArrayInputStream("{ \"foobar\" : 3 }".getBytes("UTF-8"))); + fail("Should have caught an exception"); + } catch (JsonMappingException e) { + verifyException(e, "Unrecognized field"); + } + } + +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonView.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonView.java new file mode 100644 index 0000000..1567351 --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonView.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.ByteArrayOutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.annotation.JsonView; + +public class TestJsonView extends JaxrsTestBase +{ + static class MyView1 { } + static class MyView2 { } + + static class Bean { + @JsonView(MyView1.class) + public int value1 = 1; + + @JsonView(MyView2.class) + public int value2 = 2; + } + + @JsonView({ MyView1.class }) + public void bogus() { } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + // [JACKSON-578] + public void testViews() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bean bean = new Bean(); + Method m = getClass().getDeclaredMethod("bogus"); + JsonView view = m.getAnnotation(JsonView.class); + assertNotNull(view); // just a sanity check + prov.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[] { view }, + MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("{\"value1\":1}", out.toString("UTF-8")); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonpWrapping.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonpWrapping.java new file mode 100644 index 0000000..121427c --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestJsonpWrapping.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.*; +import java.lang.annotation.Annotation; + +import javax.ws.rs.core.MediaType; + +public class TestJsonpWrapping + extends JaxrsTestBase +{ + public void testSimple() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + Object bean = new Integer[] { 1, 2, 3 }; + + // First: no JSONP wrapping: + ByteArrayOutputStream out = new ByteArrayOutputStream(); + prov.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[0], MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("[1,2,3]", out.toString("UTF-8")); + + // then with wrapping: + prov.setJSONPFunctionName("addAll"); + out = new ByteArrayOutputStream(); + prov.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[0], MediaType.APPLICATION_JSON_TYPE, null, out); + assertEquals("addAll([1,2,3])", out.toString("UTF-8")); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestRootType.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestRootType.java new file mode 100644 index 0000000..f89fdaa --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestRootType.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.ByteArrayOutputStream; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.core.type.TypeReference; + +public class TestRootType + extends JaxrsTestBase +{ + @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT, property="type") + @JsonTypeName("bean") + static class Bean { + public int a = 3; + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + public void testRootType() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + TypeReference<?> ref = new TypeReference<List<Bean>>(){}; + + Bean bean = new Bean(); + ArrayList<Bean> list = new ArrayList<Bean>(); + list.add(bean); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + MediaType mt = MediaType.APPLICATION_JSON_TYPE; + prov.writeTo(list, List.class, ref.getType(), new Annotation[0], mt, null, out); + + String json = out.toString("UTF-8"); + assertEquals("[{\"bean\":{\"a\":3}}]", json); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestStreamingOutput.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestStreamingOutput.java new file mode 100644 index 0000000..52aa90d --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestStreamingOutput.java @@ -0,0 +1,33 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.*; +import java.lang.annotation.Annotation; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.StreamingOutput; + +public class TestStreamingOutput extends JaxrsTestBase +{ + static class StreamingSubtype implements StreamingOutput + { + // important: this can trick "canSerialize()" to include it: + public int getFoo() { return 3; } + + public void write(OutputStream out) throws IOException { + out.write("OK".getBytes("UTF-8")); + } + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + public void testSimpleSubtype() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + assertFalse(prov.isWriteable(StreamingSubtype.class, StreamingSubtype.class, + new Annotation[] { }, MediaType.APPLICATION_JSON_TYPE)); + } +} diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestUntouchables.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestUntouchables.java new file mode 100644 index 0000000..c111939 --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestUntouchables.java @@ -0,0 +1,80 @@ +package com.fasterxml.jackson.jaxrs.json; + +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.util.*; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.StreamingOutput; + +/** + * Unit tests for verifying that certain JDK base types will be + * ignored by default Jackson JAX-RS conversion provider. + */ +public class TestUntouchables + extends JaxrsTestBase +{ + /** + * Test type added for [JACKSON-460]... just to ensure that "isJsonType" + * remains overridable. + */ + public static class MyJacksonJsonProvider extends JacksonJsonProvider { + // ensure isJsonType remains "protected" � this is a compile-time check. + // Some users of JacksonJsonProvider override this method; + // changing to "private" would regress them. + @Override + protected boolean isJsonType(MediaType mediaType) { return super.isJsonType(mediaType); } + } + + static class StreamingSubType implements StreamingOutput { + public void write(OutputStream output) { } + } + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + public void testDefaultUntouchables() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + // By default, no reason to exclude, say, this test class... + assertTrue(prov.isReadable(getClass(), getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + assertTrue(prov.isWriteable(getClass(), getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + + // but some types should be ignored (set of ignorable may change over time tho!) + assertFalse(prov.isWriteable(StreamingOutput.class, StreamingOutput.class, + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + assertFalse(prov.isWriteable(StreamingSubType.class, StreamingSubType.class, + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + + // and then on-the-fence things + assertFalse(prov.isReadable(String.class, getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + assertFalse(prov.isReadable(byte[].class, getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + } + + public void testCustomUntouchables() throws Exception + { + JacksonJsonProvider prov = new JacksonJsonProvider(); + // can mark this as ignorable... + prov.addUntouchable(getClass()); + // and then it shouldn't be processable + assertFalse(prov.isReadable(getClass(), getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + assertFalse(prov.isWriteable(getClass(), getClass(), + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + + // Same for interfaces, like: + prov.addUntouchable(Collection.class); + assertFalse(prov.isReadable(ArrayList.class, ArrayList.class, + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + assertFalse(prov.isWriteable(HashSet.class, HashSet.class, + new Annotation[0], MediaType.APPLICATION_JSON_TYPE)); + } +} + \ No newline at end of file diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestVersions.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestVersions.java new file mode 100644 index 0000000..be30cff --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/TestVersions.java @@ -0,0 +1,28 @@ +package com.fasterxml.jackson.jaxrs.json; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; + +public class TestVersions extends JaxrsTestBase +{ + public void testMapperVersions() + { + assertVersion(new JacksonJsonProvider()); + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private void assertVersion(Versioned vers) + { + final Version v = vers.version(); + assertFalse("Should find version information (got "+v+")", v.isUknownVersion()); + Version exp = PackageVersion.VERSION; + assertEquals(exp.toFullString(), v.toFullString()); + assertEquals(exp, v); + } +} + diff --git a/json/src/test/java/com/fasterxml/jackson/jaxrs/json/util/TestAnnotationBundleKey.java b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/util/TestAnnotationBundleKey.java new file mode 100644 index 0000000..459b29f --- /dev/null +++ b/json/src/test/java/com/fasterxml/jackson/jaxrs/json/util/TestAnnotationBundleKey.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.jaxrs.json.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import com.fasterxml.jackson.jaxrs.json.JaxrsTestBase; +import com.fasterxml.jackson.jaxrs.json.annotation.JSONP; + +public class TestAnnotationBundleKey extends JaxrsTestBase +{ + @JSONP("foo") + public void annotated1() { } + + @JSONP("foo") + public void annotated2() { } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + public void testKeys() throws Exception + { + Method m1 = getClass().getDeclaredMethod("annotated1"); + Method m2 = getClass().getDeclaredMethod("annotated2"); + + assertNotSame(m1, m2); + + Annotation[] ann1 = m1.getAnnotations(); + assertEquals(1, ann1.length); + Annotation[] ann2 = m2.getAnnotations(); + assertEquals(1, ann2.length); + + AnnotationBundleKey key1 = new AnnotationBundleKey(ann1); + AnnotationBundleKey key2 = new AnnotationBundleKey(ann2); + AnnotationBundleKey key1dup = new AnnotationBundleKey(ann1); + AnnotationBundleKey key1immutable = key1.immutableKey(); + + // identity checks first + assertEquals(key1, key1); + assertEquals(key2, key2); + assertEquals(key1dup, key1dup); + assertEquals(key1immutable, key1immutable); + + // then inequality by content (even though both have 1 JSONP annotation) + assertFalse(key1.equals(key2)); + assertFalse(key2.equals(key1)); + + // but safe copy ought to be equal + assertTrue(key1.equals(key1dup)); // from same method + assertTrue(key1dup.equals(key1)); + assertTrue(key1.equals(key1immutable)); // and immutable variant + assertTrue(key1immutable.equals(key1)); + } +} -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/jackson-jaxrs-providers.git _______________________________________________ pkg-java-commits mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-java-commits

