From 389dc9d563f285b4f4705609fe325a397b97abaf Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Tue, 29 Mar 2016 18:35:30 -0700 Subject: [PATCH] Add Guava --- README.md | 18 +- guava/README.md | 58 +++ guava/pom.xml | 82 ++++ guava/release-notes/CREDITS | 33 ++ guava/release-notes/VERSION | 202 +++++++++ .../datatype/guava/GuavaDeserializers.java | 275 ++++++++++++ .../jackson/datatype/guava/GuavaModule.java | 110 +++++ .../datatype/guava/GuavaSerializers.java | 110 +++++ .../datatype/guava/GuavaTypeModifier.java | 48 ++ .../datatype/guava/PackageVersion.java.in | 20 + .../deser/GuavaCollectionDeserializer.java | 129 ++++++ .../GuavaImmutableCollectionDeserializer.java | 76 ++++ .../deser/GuavaImmutableMapDeserializer.java | 79 ++++ .../guava/deser/GuavaMapDeserializer.java | 135 ++++++ .../deser/GuavaMultisetDeserializer.java | 70 +++ .../deser/GuavaOptionalDeserializer.java | 145 +++++++ .../guava/deser/HashCodeDeserializer.java | 23 + .../guava/deser/HashMultisetDeserializer.java | 31 ++ .../guava/deser/HostAndPortDeserializer.java | 42 ++ .../deser/ImmutableBiMapDeserializer.java | 26 ++ .../deser/ImmutableListDeserializer.java | 37 ++ .../guava/deser/ImmutableMapDeserializer.java | 31 ++ .../deser/ImmutableMultisetDeserializer.java | 27 ++ .../guava/deser/ImmutableSetDeserializer.java | 30 ++ .../deser/ImmutableSortedMapDeserializer.java | 36 ++ .../ImmutableSortedMultisetDeserializer.java | 30 ++ .../deser/ImmutableSortedSetDeserializer.java | 38 ++ .../deser/InternetDomainNameDeserializer.java | 23 + .../deser/LinkedHashMultisetDeserializer.java | 26 ++ .../guava/deser/RangeDeserializer.java | 197 +++++++++ .../guava/deser/TreeMultisetDeserializer.java | 29 ++ .../multimap/GuavaMultimapDeserializer.java | 183 ++++++++ .../list/ArrayListMultimapDeserializer.java | 43 ++ .../list/LinkedListMultimapDeserializer.java | 43 ++ .../set/HashMultimapDeserializer.java | 43 ++ .../set/LinkedHashMultimapDeserializer.java | 43 ++ .../guava/deser/util/RangeFactory.java | 181 ++++++++ .../ser/GuavaBeanSerializerModifier.java | 30 ++ .../ser/GuavaOptionalBeanPropertyWriter.java | 43 ++ .../guava/ser/GuavaOptionalSerializer.java | 347 +++++++++++++++ ...aUnwrappingOptionalBeanPropertyWriter.java | 42 ++ .../guava/ser/MultimapSerializer.java | 409 ++++++++++++++++++ .../datatype/guava/ser/RangeSerializer.java | 149 +++++++ .../datatype/guava/ser/TableSerializer.java | 233 ++++++++++ .../com.fasterxml.jackson.databind.Module | 1 + .../datatype/guava/FluentIterableTest.java | 46 ++ .../jackson/datatype/guava/HashCodeTest.java | 32 ++ .../datatype/guava/HostAndPortTest.java | 41 ++ .../jackson/datatype/guava/IterablesTest.java | 50 +++ .../datatype/guava/ModuleTestBase.java | 46 ++ .../datatype/guava/ScalarTypesTest.java | 25 ++ .../guava/TableSerializationTest.java | 165 +++++++ .../datatype/guava/TestImmutables.java | 244 +++++++++++ .../jackson/datatype/guava/TestMultimaps.java | 312 +++++++++++++ .../jackson/datatype/guava/TestMultisets.java | 125 ++++++ .../jackson/datatype/guava/TestRange.java | 252 +++++++++++ .../jackson/datatype/guava/TestVersions.java | 37 ++ .../guava/failing/OptionalUnwrappedTest.java | 39 ++ .../guava/optional/OptionalBasicTest.java | 321 ++++++++++++++ .../guava/optional/OptionalSchema83Test.java | 131 ++++++ .../guava/optional/OptionalUnwrappedTest.java | 37 ++ .../optional/TestOptionalWithPolymorphic.java | 123 ++++++ .../jackson/datatype/guava/pojo/AddOp.java | 58 +++ .../jackson/datatype/guava/pojo/MathOp.java | 17 + .../jackson/datatype/guava/pojo/MulOp.java | 58 +++ pom.xml | 20 +- 66 files changed, 6181 insertions(+), 4 deletions(-) create mode 100644 guava/README.md create mode 100644 guava/pom.xml create mode 100644 guava/release-notes/CREDITS create mode 100644 guava/release-notes/VERSION create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaModule.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/PackageVersion.java.in create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaCollectionDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableCollectionDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableMapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaOptionalDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashCodeDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HostAndPortDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableBiMapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableListDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedSetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/InternetDomainNameDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/LinkedHashMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/RangeDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/TreeMultisetDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/GuavaMultimapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/ArrayListMultimapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/LinkedListMultimapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/HashMultimapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/LinkedHashMultimapDeserializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/util/RangeFactory.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaBeanSerializerModifier.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalBeanPropertyWriter.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalSerializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaUnwrappingOptionalBeanPropertyWriter.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/MultimapSerializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/RangeSerializer.java create mode 100644 guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java create mode 100644 guava/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/FluentIterableTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/HashCodeTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/HostAndPortTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/IterablesTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/ModuleTestBase.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/ScalarTypesTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TableSerializationTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestImmutables.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultimaps.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultisets.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestRange.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestVersions.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/failing/OptionalUnwrappedTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalBasicTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalSchema83Test.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalUnwrappedTest.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/TestOptionalWithPolymorphic.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/AddOp.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MathOp.java create mode 100644 guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MulOp.java diff --git a/README.md b/README.md index 5a4959ff..15a81fb7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Datatype modules to support 3rd party Collection libraries. Currently included are: +* [Guava](guava/) datatype ([Guava](http://code.google.com/p/guava-libraries/)) * [HPPC](hppc/) datatype ([High-Performance Primitive Collections](https://labs.carrotsearch.com/hppc.html)) * [PCollections](pcollections/) datatype ([Persistent Java Collections](http://pcollections.org)) @@ -14,6 +15,19 @@ All modules are licensed under [Apache License 2.0](http://www.apache.org/licens [![Build Status](https://travis-ci.org/FasterXML/jackson-datatypes-collections.svg)](https://travis-ci.org/FasterXML/jackson-datatypes-collections) -## More +## Usage + +Like all standard Jackson modules (libraries that implement Module interface), registration for Collections +datatypes is done as follows: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new GuavaModule() + .registerModule(new HppcModule()) + .registerModule(new PCollectionsModule()) + ; +``` + +after which datatype read/write support is available for all normal Jackson operations, +including support for nested types. -See [Wiki](../../wiki) for more information (javadocs). diff --git a/guava/README.md b/guava/README.md new file mode 100644 index 00000000..29811c84 --- /dev/null +++ b/guava/README.md @@ -0,0 +1,58 @@ +[Jackson](../../../jackson) datatype module (jar) +to support JSON serialization and deserialization of +[Guava](http://code.google.com/p/guava-libraries/) data types. + +## Status + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.datatype/jackson-datatype-guava/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.fasterxml.jackson.datatype/jackson-datatype-guava/) +[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/com.fasterxml.jackson.datatype/jackson-datatype-guava/badge.svg)](http://www.javadoc.io/doc/com.fasterxml.jackson.datatype/jackson-datatype-guava) + +Module has been production-ready since version 2.3. +Not all datatypes of Guava are support due to sheer size of the library; new support is added based on contributions. + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +## Usage + +### Maven dependency + +To use module on Maven-based projects, use following dependency: + +```xml + + com.fasterxml.jackson.datatype + jackson-datatype-guava + 2.7.3 + +``` + +(or whatever version is most up-to-date at the moment) + +### Registering module + +Like all standard Jackson modules (libraries that implement Module interface), registration is done as follows: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new GuavaModule()); +``` + +after which functionality is available for all normal Jackson operations. + +### Configuration + +Configurable settings of the module are: + +* `configureAbsentsAsNulls` (default: true) (added in 2.6) + * If enabled, will consider `Optional.absent()` to be "null-equivalent", and NOT serialized if inclusion is defined as `Include.NON_NULL` + * If disabled, `Optional.absent()` behaves as standard referential type, and is included with `Include.NON_NULL` + * In either case, `Optional.absent()` values are always excluded with Inclusion values of: + * NON_EMPTY + * NON_ABSENT (new in Jackson 2.6) + +## More + +See [Wiki](../../wiki) for more information (javadocs, downloads). + diff --git a/guava/pom.xml b/guava/pom.xml new file mode 100644 index 00000000..6f2115a8 --- /dev/null +++ b/guava/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + + com.fasterxml.jackson.datatype + jackson-datatypes-collections + 2.7.4-SNAPSHOT + + jackson-datatype-guava + Jackson datatype: Guava + bundle + Add-on datatype-support module for Jackson (https://github.com/FasterXML/jackson) that handles +Guava (http://code.google.com/p/guava-libraries/) types (currently mostly just collection ones) + + https://github.com/FasterXML/jackson-datatypes-collections + + + + Steven Schlansker + steven@nesscomputing.com + + + + + + 16.0 + + + [${version.guava}.0,20) + + + com/fasterxml/jackson/datatype/guava + ${project.groupId}.guava + + +com.google.common.*;version="${version.guava.osgi}", +* + + + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + com.google.guava + guava + ${version.guava} + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + com/fasterxml/jackson/**/failing/*.java + + + + + + diff --git a/guava/release-notes/CREDITS b/guava/release-notes/CREDITS new file mode 100644 index 00000000..5c3f82f2 --- /dev/null +++ b/guava/release-notes/CREDITS @@ -0,0 +1,33 @@ +Here are people who have contributed to the development of Jackson JSON processor +Guava datatype component, version 2.x +(version numbers in brackets indicate release in which the problem was fixed) + +Tatu Saloranta (tatu.saloranta@iki.fi): author + +Steven Schlansker (steven@nesscomputing.com): co-author + +Stephan Schroevers: (Stephan202@github) + * Contributed #56: Add support `HashCode` + (2.5.0) + * Contributed #65: Add deserialization support for SortedMultiset and ImmutableSortedMultiset + (2.5.3) + +Michael Hixson: + * Contributed fix for #67: Support deserializing ImmutableSetMultimaps + (2.6.0) + +Alexey Kobyakov (akobiakov@github) + * Reported #64: `@JsonUnwrapped` annotation is ignored when a field is an Optional + (2.6.0) + +Jose Thomas (josethomas@github) + * Contributed #52: Guava collection types do not allow null values + (2.6.2) + +Jonathan Rodrigues de Oliveira (jorool@github) + * Contributed #79: New configuration for Guava Range default bound type. + (2.7.0) + +Micahel Jameson (mjameson-se@github) + * Reported #87: OSGi import missing for `com.fasterxml.jackson.annotation.JsonInclude$Value` + (2.7.1) diff --git a/guava/release-notes/VERSION b/guava/release-notes/VERSION new file mode 100644 index 00000000..fb3cca5a --- /dev/null +++ b/guava/release-notes/VERSION @@ -0,0 +1,202 @@ +Project: jackson-datatype-guava + +------------------------------------------------------------------------ +=== Releases === +------------------------------------------------------------------------ + +2.7.4 (not yet released) + +- Improve handling of `Optional` to ensure custom content serializer/deserializer + is used. + +2.7.3 (16-Mar-2016) + +No changes since 2.7.2 + +2.7.2 (27-Feb-2016) + +- Change build to produce JDK6-compatible jar, to allow use on JDK6 runtime + +2.7.1 (02-Feb-2016) + +#87, #88: OSGi import missing for `com.fasterxml.jackson.annotation.JsonInclude$Value` + (reported by Michael J) + +2.7.0 (10-Jan-2016) + +#76: New configuration for Guava Range default bound type. + (contributed by Jonathan R-d-O) +(partial, serialization works) #11: Add support for Table +- Simplify OSGi settings for build. +- Guava version now 16 (from 15); does not rely on new features yet + +2.6.4 (not yet released) + +- Small fix to unwrapped `Optional` serialization, should not fail when trying to + serialize empty unwrapped `Optional`. + +2.6.3 (12-Oct-2015) + +#83: Generic type information for `Optional` wrapped generic type lost in visitor + (reported by vzx@github) +#84: Class not found exception in OSGi (com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter) + (reported by mjameson-se@github) +- Allow use of `@JsonDeserializer(contentAs=...)` with `Optional` + +2.6.2 (15-Sep-2015) + +#52: Guava collection types do not allow null values + (contributed by Jose T) +#80: Relax OSGi version constraints for Guava dependency. + (requested by Benson M) +#82: Problem with polymorphic value types for `Optional`, with 2.6 + +2.6.1 (09-Aug-2015) + +No changes since 2.6.0 + +2.6.0 (19-Jul-2015) + +#64: `@JsonUnwrapped` annotation is ignored when a field is an Optional + (reported by Alexey K, akobiakov@github) +#66: Add `GuavaModule.configureAbsentsAsNulls(boolean)` to change whether + `Optional.absent()` is to be handled same as Java null during serialization + (default: true) or not. +#67: Support deserializing ImmutableSetMultimaps + (contributed by Michael H, michaelhixson@github) +#69: Add support for `JsonInclude.Include.NON_ABSENT`, to compensate for #66 +#70: Change OSGi manifest entries to import guava 15.0 or greater + (reported by sprynter@github) +#74: Multimap serializer ignores _valueTypeSerializer + (reported by soldierkam@github) + +2.5.4 (09-Jun-2015) + +- Add override for `MultimapSerializer.isEmpty(SerializerProvider, Multimap)` + for forward compatibility (needed for databind 2.6) + +2.5.3 (24-Apr-2015) + +#65: Add deserialization support for SortedMultiset and ImmutableSortedMultiset + (contributed by Stephan S, Stephan202@github) + +2.5.2 (29-Mar-2015) + +#62: Add `com.google.common.hash` to OSGi import list + (reported by lukewink@github) + +2.5.1 (06-Fev-2015) + +No changes since 2.5.0 + +2.5.0 (01-Jan-2015) + +#56: Add support `HashCode` + (contributed by Stephan S, Stephan202@github) + +2.4.6 (23-Apr-2015) + +#61: NPE serializing `Multimap`s with null values + (reported by sixinli@github) + +2.4.5 (13-Jan-2015) + +#58: `FluentIterable` serialization doesn't work for Bean properties + (fixed by Christopher C) + +2.4.4 (24-Nov-2014) + +No changes since 2.4.3 + +2.4.3 (04-Oct-2014) + +#50: Add support for `InternetDomainName` + (suggested by sdavids@github) +- Improved serialization, type handling, schema-access for `Range` and `Optional`. + +2.4.2 (15-Aug-2014) + +#46: Can not serialize guava Iterables + (reported by chisui@github) + +2.4.1 (17-Jun-2014) + +No changes since 2.4.0. + +2.4.0 (03-Jun-2014) + +#43: Add support for `HostAndPort` + +2.3.3 (14-Apr-2014) + +#37: `Optional` not correctly deserialized from JSON null, if inside a Collection + (reported by JYang-Addepar@github) +#41: `Multimap` serializer does not honor @JsonInclude(JsonInclude.Include.NON_EMPTY) + (reported by Olve S-H) + +2.3.2 (01-Mar-2014) + +#36: Improve Range deserializer to work with older Guava versions (10-) + (contribtued by ispringer@github) + +2.3.1 (28-Dec-2013) + +#33: Add support for `Range` values + (contribute by ispringer@github) +#34: Use Optional type parameter if present to create JSON schema + (contributed by cponomaryov@github) + +2.3.0 (14-Nov-2013) + +#29: Empty ImmutableMap not deserialized correctly, when type info included + (reported by pbergn@github) +- Add support for `DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY` for + `ImmutableSet` and `MultiSet` + +2.2.3 (25-Aug-2013) +2.2.2 (27-May-2013) +2.2.1 (03-May-2013) + +No functional changes. + +2.2.0 (23-Apr-2013) + +New minor version, no functional changes. + +2.1.2 (08-Dec-2012) + +No functional changes. + +2.1.1 (13-Nov-2012) + +* More improvements to handling of Optional values + +2.1.0 (08-Oct-2012) + +* [Issue#9]: Handling of Optional values, wrt NON_NULL inclusion + +2.0.6 (30-Sep-2012) + + No functional changes, just dependency updates. + +2.0.4, 2.0.5: not released + +2.0.3 (14-Jun-2012) + +* Issue-6: An NPE in MultimapDeserializer + +2.0.2 (16-May-2012) + +Fixes: + +* Issue-3: (Tree)MultiMaps not serialized properly + +Improvements: + +* Add for a wider set of Collections + (contributed by Pascal G) + + +2.0.0 (25-Mar-2012) + +The official 2.0 release... diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java new file mode 100644 index 00000000..bb492fe9 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaDeserializers.java @@ -0,0 +1,275 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.google.common.base.Optional; +import com.google.common.collect.*; +import com.google.common.hash.HashCode; +import com.google.common.net.HostAndPort; +import com.google.common.net.InternetDomainName; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.Deserializers; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.ReferenceType; +import com.fasterxml.jackson.datatype.guava.deser.*; +import com.fasterxml.jackson.datatype.guava.deser.multimap.list.ArrayListMultimapDeserializer; +import com.fasterxml.jackson.datatype.guava.deser.multimap.list.LinkedListMultimapDeserializer; +import com.fasterxml.jackson.datatype.guava.deser.multimap.set.HashMultimapDeserializer; +import com.fasterxml.jackson.datatype.guava.deser.multimap.set.LinkedHashMultimapDeserializer; + +/** + * Custom deserializers module offers. + */ +public class GuavaDeserializers + extends Deserializers.Base +{ + protected BoundType _defaultBoundType; + + public GuavaDeserializers() { + this(null); + } + + public GuavaDeserializers(BoundType defaultBoundType) { + _defaultBoundType = defaultBoundType; + } + + /** + * We have plenty of collection types to support... + */ + @Override + public JsonDeserializer findCollectionDeserializer(CollectionType type, + DeserializationConfig config, BeanDescription beanDesc, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) + throws JsonMappingException + { + Class raw = type.getRawClass(); + + // ImmutableXxx types? + if (ImmutableCollection.class.isAssignableFrom(raw)) { + if (ImmutableList.class.isAssignableFrom(raw)) { + return new ImmutableListDeserializer(type, + elementTypeDeserializer, elementDeserializer); + } + if (ImmutableMultiset.class.isAssignableFrom(raw)) { + // sorted one? + if (ImmutableSortedMultiset.class.isAssignableFrom(raw)) { + /* See considerations for ImmutableSortedSet below. */ + requireCollectionOfComparableElements(type, "ImmutableSortedMultiset"); + return new ImmutableSortedMultisetDeserializer(type, + elementTypeDeserializer, elementDeserializer); + } + // nah, just regular one + return new ImmutableMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + if (ImmutableSet.class.isAssignableFrom(raw)) { + // sorted one? + if (ImmutableSortedSet.class.isAssignableFrom(raw)) { + /* 28-Nov-2010, tatu: With some more work would be able to use other things + * than natural ordering; but that'll have to do for now... + */ + requireCollectionOfComparableElements(type, "ImmutableSortedSet"); + return new ImmutableSortedSetDeserializer(type, + elementTypeDeserializer, elementDeserializer); + } + // nah, just regular one + return new ImmutableSetDeserializer(type, + elementTypeDeserializer, elementDeserializer); + } + // TODO: make configurable (for now just default blindly to a list) + return new ImmutableListDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + + // Multi-xxx collections? + if (Multiset.class.isAssignableFrom(raw)) { + if (SortedMultiset.class.isAssignableFrom(raw)) { + if (TreeMultiset.class.isAssignableFrom(raw)) { + return new TreeMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + + // TODO: make configurable (for now just default blindly) + return new TreeMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + + // Quite a few variations... + if (LinkedHashMultiset.class.isAssignableFrom(raw)) { + return new LinkedHashMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + if (HashMultiset.class.isAssignableFrom(raw)) { + return new HashMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + if (EnumMultiset.class.isAssignableFrom(raw)) { + // !!! TODO + } + + // TODO: make configurable (for now just default blindly) + return new HashMultisetDeserializer(type, elementTypeDeserializer, elementDeserializer); + } + + return null; + } + + private void requireCollectionOfComparableElements(CollectionType actualType, String targetType) { + Class elemType = actualType.getContentType().getRawClass(); + if (!Comparable.class.isAssignableFrom(elemType)) { + throw new IllegalArgumentException("Can not handle " + targetType + + " with elements that are not Comparable (" + elemType.getName() + ")"); + } + } + + /** + * A few Map types to support. + */ + @Override + public JsonDeserializer findMapDeserializer(MapType type, + DeserializationConfig config, BeanDescription beanDesc, + KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) + throws JsonMappingException + { + Class raw = type.getRawClass(); + + // ImmutableXxxMap types? + if (ImmutableMap.class.isAssignableFrom(raw)) { + if (ImmutableSortedMap.class.isAssignableFrom(raw)) { + return new ImmutableSortedMapDeserializer(type, keyDeserializer, elementTypeDeserializer, + elementDeserializer); + } + if (ImmutableBiMap.class.isAssignableFrom(raw)) { + return new ImmutableBiMapDeserializer(type, keyDeserializer, elementTypeDeserializer, + elementDeserializer); + } + // Otherwise, plain old ImmutableMap... + return new ImmutableMapDeserializer(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + // XxxBiMap types? + if (BiMap.class.isAssignableFrom(raw)) { + if (EnumBiMap.class.isAssignableFrom(raw)) { + // !!! TODO + } + if (EnumHashBiMap.class.isAssignableFrom(raw)) { + // !!! TODO + } + if (HashBiMap.class.isAssignableFrom(raw)) { + // !!! TODO + } + // !!! TODO default + } + + + return null; + } + + @Override + public JsonDeserializer findMapLikeDeserializer(MapLikeType type, + DeserializationConfig config, BeanDescription beanDesc, + KeyDeserializer keyDeserializer, TypeDeserializer elementTypeDeserializer, + JsonDeserializer elementDeserializer) + throws JsonMappingException + { + Class raw = type.getRawClass(); + + // ListMultimaps + if (ListMultimap.class.isAssignableFrom(raw)) { + if (ImmutableListMultimap.class.isAssignableFrom(raw)) { + // TODO + } + if (ArrayListMultimap.class.isAssignableFrom(raw)) { + return new ArrayListMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + if (LinkedListMultimap.class.isAssignableFrom(raw)) { + return new LinkedListMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + if (ForwardingListMultimap.class.isAssignableFrom(raw)) { + // TODO + } + + // TODO: Remove the default fall-through once all implementations are in place. + return new ArrayListMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + + // SetMultimaps + if (SetMultimap.class.isAssignableFrom(raw)) { + + // SortedSetMultimap + if (SortedSetMultimap.class.isAssignableFrom(raw)) { + if (TreeMultimap.class.isAssignableFrom(raw)) { + // TODO + } + if (ForwardingSortedSetMultimap.class.isAssignableFrom(raw)) { + // TODO + } + } + + if (ImmutableSetMultimap.class.isAssignableFrom(raw)) { + // [Issue#67]: Preserve order of entries + return new LinkedHashMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + if (HashMultimap.class.isAssignableFrom(raw)) { + return new HashMultimapDeserializer(type, keyDeserializer, elementTypeDeserializer, + elementDeserializer); + } + if (LinkedHashMultimap.class.isAssignableFrom(raw)) { + return new LinkedHashMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + if (ForwardingSetMultimap.class.isAssignableFrom(raw)) { + // TODO + } + + // TODO: Remove the default fall-through once all implementations are covered. + return new HashMultimapDeserializer(type, keyDeserializer, elementTypeDeserializer, + elementDeserializer); + } + + // Handle the case where nothing more specific was provided. + if (Multimap.class.isAssignableFrom(raw)) { + return new LinkedListMultimapDeserializer(type, keyDeserializer, + elementTypeDeserializer, elementDeserializer); + } + + if (Table.class.isAssignableFrom(raw)) { + // !!! TODO + } + + return null; + } + + // 21-Oct-2015, tatu: Code much simplified with 2.7 where we should be getting much + // of boilerplate handling automatically + + @Override // since 2.7 + public JsonDeserializer findReferenceDeserializer(ReferenceType refType, + DeserializationConfig config, BeanDescription beanDesc, + TypeDeserializer contentTypeDeserializer, JsonDeserializer contentDeserializer) + { + if (refType.hasRawClass(Optional.class)) { + return new GuavaOptionalDeserializer(refType, contentTypeDeserializer, contentDeserializer); + } + return null; + } + + @Override + public JsonDeserializer findBeanDeserializer(final JavaType type, DeserializationConfig config, + BeanDescription beanDesc) + { + if (type.hasRawClass(Range.class)) { + return new RangeDeserializer(_defaultBoundType, type); + } + if (type.hasRawClass(HostAndPort.class)) { + return HostAndPortDeserializer.std; + } + if (type.hasRawClass(InternetDomainName.class)) { + return InternetDomainNameDeserializer.std; + } + if (type.hasRawClass(HashCode.class)) { + return HashCodeDeserializer.std; + } + return null; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaModule.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaModule.java new file mode 100644 index 00000000..b82f3c7f --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaModule.java @@ -0,0 +1,110 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.core.Version; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.datatype.guava.ser.GuavaBeanSerializerModifier; +import com.google.common.collect.BoundType; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Basic Jackson {@link Module} that adds support for Guava types. + *

+ * Current configurability includes: + *

+ */ +public class GuavaModule extends Module // can't use just SimpleModule, due to generic types +{ + private final String NAME = "GuavaModule"; + + /** + * Configuration setting that determines whether `Optional.absent()` is + * considered "same as null" for serialization purposes; that is, to be + * filtered same as nulls are. + * If enabled, absent values are treated like nulls; if disabled, they are not. + * In either case, absent values are always considered "empty". + *

+ * Default value is `true` for backwards compatibility (2.5 and prior + * only had this behavior). + *

+ * Note that this setting MUST be changed BEFORE registering the module: + * changes after registration will have no effect. + */ + protected boolean _cfgHandleAbsentAsNull = true; + protected BoundType _defaultBoundType; + + public GuavaModule() { + super(); + } + + @Override public String getModuleName() { return NAME; } + @Override public Version version() { return PackageVersion.VERSION; } + + @Override + public void setupModule(SetupContext context) + { + context.addDeserializers(new GuavaDeserializers(_defaultBoundType)); + context.addSerializers(new GuavaSerializers()); + context.addTypeModifier(new GuavaTypeModifier()); + + // 28-Apr-2015, tatu: Allow disabling "treat Optional.absent() like Java nulls" + if (_cfgHandleAbsentAsNull) { + context.addBeanSerializerModifier(new GuavaBeanSerializerModifier()); + } + } + + /** + * Configuration method that may be used to change configuration setting + * _cfgHandleAbsentAsNull: enabling means that `Optional.absent()` values + * are handled like Java nulls (wrt filtering on serialization); disabling that + * they are only treated as "empty" values, but not like native Java nulls. + * Recommended setting for this value is `false`, for compatibility with other + * "optional" values (like JDK 8 optionals); but the default is `true` for + * backwards compatibility. + * + * @return This module instance, useful for chaining calls + * + * @since 2.6 + */ + public GuavaModule configureAbsentsAsNulls(boolean state) { + _cfgHandleAbsentAsNull = state; + return this; + } + + /** + * Configuration method that may be used to change the {@link BoundType} to be used + * when deserializing {@link com.google.common.collect.Range} objects. This configuration + * will is used when the object to be deserialized has no bound type attribute. + * The default {@link BoundType} is CLOSED. + * + * @param boundType {@link BoundType} + * + * @return This module instance, useful for chaining calls + + * @since 2.7 + */ + public GuavaModule defaultBoundType(BoundType boundType) { + checkNotNull(boundType); + _defaultBoundType = boundType; + return this; + } + + @Override + public int hashCode() { + return NAME.hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java new file mode 100644 index 00000000..f7becfdc --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaSerializers.java @@ -0,0 +1,110 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.util.HashSet; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.Serializers; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.databind.type.ReferenceType; +import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer; +import com.fasterxml.jackson.databind.util.ArrayBuilders; +import com.fasterxml.jackson.databind.util.StdConverter; +import com.google.common.base.Optional; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Multimap; +import com.google.common.collect.Range; +import com.google.common.collect.Table; +import com.google.common.hash.HashCode; +import com.google.common.net.HostAndPort; +import com.google.common.net.InternetDomainName; +import com.fasterxml.jackson.datatype.guava.ser.GuavaOptionalSerializer; +import com.fasterxml.jackson.datatype.guava.ser.MultimapSerializer; +import com.fasterxml.jackson.datatype.guava.ser.RangeSerializer; +import com.fasterxml.jackson.datatype.guava.ser.TableSerializer; + +public class GuavaSerializers extends Serializers.Base +{ + static class FluentConverter extends StdConverter> { + static final FluentConverter instance = new FluentConverter(); + + @Override + public Iterable convert(Object value) { + return (Iterable) value; + } + } + + @Override + public JsonSerializer findReferenceSerializer(SerializationConfig config, + ReferenceType refType, BeanDescription beanDesc, + TypeSerializer contentTypeSerializer, JsonSerializer contentValueSerializer) + { + final Class raw = refType.getRawClass(); + if (Optional.class.isAssignableFrom(raw)) { + return new GuavaOptionalSerializer(refType, contentTypeSerializer, contentValueSerializer); + } + return null; + } + + @Override + public JsonSerializer findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) + { + Class raw = type.getRawClass(); + if (Range.class.isAssignableFrom(raw)) { + return new RangeSerializer(_findDeclared(type, Range.class)); + } + if (Table.class.isAssignableFrom(raw)) { + return new TableSerializer(_findDeclared(type, Table.class)); + } + + // since 2.4 + if (HostAndPort.class.isAssignableFrom(raw)) { + return ToStringSerializer.instance; + } + if (InternetDomainName.class.isAssignableFrom(raw)) { + return ToStringSerializer.instance; + } + // not sure how useful, but why not? + if (CacheBuilderSpec.class.isAssignableFrom(raw) || CacheBuilder.class.isAssignableFrom(raw)) { + return ToStringSerializer.instance; + } + if (HashCode.class.isAssignableFrom(raw)) { + return ToStringSerializer.instance; + } + if (FluentIterable.class.isAssignableFrom(raw)) { + JavaType iterableType = _findDeclared(type, Iterable.class); + return new StdDelegatingSerializer(FluentConverter.instance, iterableType, null); + } + return super.findSerializer(config, type, beanDesc); + } + + @Override + public JsonSerializer findMapLikeSerializer(SerializationConfig config, + MapLikeType type, BeanDescription beanDesc, JsonSerializer keySerializer, + TypeSerializer elementTypeSerializer, JsonSerializer elementValueSerializer) + { + if (Multimap.class.isAssignableFrom(type.getRawClass())) { + final AnnotationIntrospector intr = config.getAnnotationIntrospector(); + Object filterId = intr.findFilterId((Annotated)beanDesc.getClassInfo()); + String[] ignored = intr.findPropertiesToIgnore(beanDesc.getClassInfo(), true); + HashSet ignoredEntries = (ignored == null || ignored.length == 0) + ? null : ArrayBuilders.arrayToSet(ignored); + + return new MultimapSerializer(type, beanDesc, + keySerializer, elementTypeSerializer, elementValueSerializer, ignoredEntries, filterId); + } + return null; + } + + private JavaType _findDeclared(JavaType subtype, Class target) { + JavaType decl = subtype.findSuperType(target); + if (decl == null) { // should never happen but + throw new IllegalArgumentException("Strange "+target.getName()+" sub-type, "+subtype+", can not find type parameters"); + } + return decl; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java new file mode 100644 index 00000000..554b2205 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/GuavaTypeModifier.java @@ -0,0 +1,48 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.lang.reflect.Type; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.*; + +import com.google.common.base.Optional; +import com.google.common.collect.*; + +/** + * We need somewhat hacky support for following Guava types: + *
    + *
  • FluentIterable: addition of seeming "empty" property should not prevent serialization as + * basic `Iterable` (with standard Jackson (de)serializer) + *
  • + *
  • Multimap: can reuse much/most of standard Map support as long as we make sure it is + * recognized as "Map-like" type (similar to how Scala Maps are supported) + *
  • + *
  • Optional: generic type, simpler, more-efficient to detect parameterization here (although + * not strictly mandatory) + *
  • Range: same as with Optional, might as well resolve generic type information early on + *
  • + *
  • + *
+ */ +public class GuavaTypeModifier extends TypeModifier +{ + @Override + public JavaType modifyType(JavaType type, Type jdkType, TypeBindings bindings, TypeFactory typeFactory) + { + if (type.isReferenceType() || type.isContainerType()) { + return type; + } + + final Class raw = type.getRawClass(); + // First: make Multimaps look more Map-like + if (raw == Multimap.class) { + return MapLikeType.upgradeFrom(type, + type.containedTypeOrUnknown(0), + type.containedTypeOrUnknown(1)); + } + if (raw == Optional.class) { + return ReferenceType.upgradeFrom(type, type.containedTypeOrUnknown(0)); + } + return type; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/PackageVersion.java.in b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/PackageVersion.java.in new file mode 100644 index 00000000..7860aa14 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/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/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaCollectionDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaCollectionDeserializer.java new file mode 100644 index 00000000..051022b2 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaCollectionDeserializer.java @@ -0,0 +1,129 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; + +public abstract class GuavaCollectionDeserializer + extends StdDeserializer + implements ContextualDeserializer +{ + private static final long serialVersionUID = 1L; + + protected final CollectionType _containerType; + + /** + * Deserializer used for values contained in collection being deserialized; + * either assigned on constructor, or during resolve(). + */ + protected final JsonDeserializer _valueDeserializer; + + /** + * If value instances have polymorphic type information, this + * is the type deserializer that can deserialize required type + * information + */ + protected final TypeDeserializer _typeDeserializerForValue; + + protected GuavaCollectionDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + super(type); + _containerType = type; + _typeDeserializerForValue = typeDeser; + _valueDeserializer = deser; + } + + /** + * Overridable fluent factory method used for creating contextual + * instances. + */ + public abstract GuavaCollectionDeserializer withResolved( + TypeDeserializer typeDeser, JsonDeserializer valueDeser); + + /* + /********************************************************** + /* Validation, post-processing + /********************************************************** + */ + + /** + * Method called to finalize setup of this deserializer, + * after deserializer itself has been registered. This + * is needed to handle recursive and transitive dependencies. + */ + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException + { + JsonDeserializer deser = _valueDeserializer; + TypeDeserializer typeDeser = _typeDeserializerForValue; + if (deser == null) { + deser = ctxt.findContextualValueDeserializer(_containerType.getContentType(), property); + } + if (typeDeser != null) { + typeDeser = typeDeser.forProperty(property); + } + if (deser == _valueDeserializer && typeDeser == _typeDeserializerForValue) { + return this; + } + return withResolved(typeDeser, deser); + } + + /* + /********************************************************** + /* Deserialization interface + /********************************************************** + */ + + /** + * Base implementation that does not assume specific type + * inclusion mechanism. Sub-classes are expected to override + * this method if they are to handle type information. + */ + @Override + public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) + throws IOException, JsonProcessingException + { + return typeDeserializer.deserializeTypedFromArray(jp, ctxt); + } + + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + // Should usually point to START_ARRAY + if (jp.isExpectedStartArrayToken()) { + return _deserializeContents(jp, ctxt); + } + // But may support implicit arrays from single values? + if (ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)) { + return _deserializeFromSingleValue(jp, ctxt); + } + throw ctxt.mappingException(_containerType.getRawClass()); + } + + /* + /********************************************************************** + /* Abstract methods for impl classes + /********************************************************************** + */ + + protected abstract T _deserializeContents(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException; + + /** + * Method used to support implicit coercion from a single non-array value + * into single-element collection. + * + * @since 2.3 + */ + protected abstract T _deserializeFromSingleValue(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException; +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableCollectionDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableCollectionDeserializer.java new file mode 100644 index 00000000..b5de17cd --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableCollectionDeserializer.java @@ -0,0 +1,76 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.ImmutableCollection; + +abstract class GuavaImmutableCollectionDeserializer> + extends GuavaCollectionDeserializer +{ + private static final long serialVersionUID = 1L; + + GuavaImmutableCollectionDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + protected abstract ImmutableCollection.Builder createBuilder(); + + @Override + protected T _deserializeContents(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonDeserializer valueDes = _valueDeserializer; + JsonToken t; + final TypeDeserializer typeDeser = _typeDeserializerForValue; + // No way to pass actual type parameter; but does not matter, just + // compiler-time fluff: + ImmutableCollection.Builder builder = createBuilder(); + + while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { + Object value; + + if (t == JsonToken.VALUE_NULL) { + value = null; + } else if (typeDeser == null) { + value = valueDes.deserialize(jp, ctxt); + } else { + value = valueDes.deserializeWithType(jp, ctxt, typeDeser); + } + builder.add(value); + } + // No class outside of the package will be able to subclass us, + // and we provide the proper builder for the subclasses we implement. + @SuppressWarnings("unchecked") + T collection = (T) builder.build(); + return collection; + } + + @Override + protected T _deserializeFromSingleValue(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + JsonDeserializer valueDes = _valueDeserializer; + final TypeDeserializer typeDeser = _typeDeserializerForValue; + JsonToken t = jp.getCurrentToken(); + + Object value; + + if (t == JsonToken.VALUE_NULL) { + value = null; + } else if (typeDeser == null) { + value = valueDes.deserialize(jp, ctxt); + } else { + value = valueDes.deserializeWithType(jp, ctxt, typeDeser); + } + @SuppressWarnings("unchecked") + T result = (T) createBuilder().add(value).build(); + return result; + } +} \ No newline at end of file diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableMapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableMapDeserializer.java new file mode 100644 index 00000000..f47316e1 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaImmutableMapDeserializer.java @@ -0,0 +1,79 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.google.common.collect.ImmutableMap; + +abstract class GuavaImmutableMapDeserializer> extends + GuavaMapDeserializer { + + GuavaImmutableMapDeserializer(MapType type, KeyDeserializer keyDeser, TypeDeserializer typeDeser, + JsonDeserializer deser) { + super(type, keyDeser, typeDeser, deser); + } + + protected abstract ImmutableMap.Builder createBuilder(); + + @Override + protected T _deserializeEntries(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + final KeyDeserializer keyDes = _keyDeserializer; + final JsonDeserializer valueDes = _valueDeserializer; + final TypeDeserializer typeDeser = _typeDeserializerForValue; + + ImmutableMap.Builder builder = createBuilder(); + for (; p.getCurrentToken() == JsonToken.FIELD_NAME; p.nextToken()) { + // Must point to field name now + String fieldName = p.getCurrentName(); + Object key = (keyDes == null) ? fieldName : keyDes.deserializeKey(fieldName, ctxt); + // And then the value... + JsonToken t = p.nextToken(); + // 28-Nov-2010, tatu: Should probably support "ignorable properties" in future... + Object value; + if (t == JsonToken.VALUE_NULL) { + _handleNull(ctxt, key, _valueDeserializer, builder); + continue; + } + if (typeDeser == null) { + value = valueDes.deserialize(p, ctxt); + } else { + value = valueDes.deserializeWithType(p, ctxt, typeDeser); + } + builder.put(key, value); + } + // No class outside of the package will be able to subclass us, + // and we provide the proper builder for the subclasses we implement. + @SuppressWarnings("unchecked") + T map = (T) builder.build(); + return map; + } + + /** + * Overridable helper method called when a JSON null value is encountered. + * Since Guava Maps typically do not allow null values, special handling + * is needed; default is to simply ignore and skip such values, but alternative + * could be to throw an exception. + */ + protected void _handleNull(DeserializationContext ctxt, Object key, + JsonDeserializer valueDeser, + ImmutableMap.Builder builder) throws IOException + { + // 14-Sep-2015, tatu: As per [datatype-guava#52], avoid exception due to null + // TODO: allow reporting problem via a feature, in future? + + // Actually, first, see if there's an alternative to Java null + Object nvl = valueDeser.getNullValue(ctxt); + if (nvl != null) { + builder.put(key, nvl); + } + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMapDeserializer.java new file mode 100644 index 00000000..9075756d --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMapDeserializer.java @@ -0,0 +1,135 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapType; + +public abstract class GuavaMapDeserializer + extends JsonDeserializer + implements ContextualDeserializer +{ + protected final MapType _mapType; + + /** + * Key deserializer used, if not null. If null, String from JSON + * content is used as is. + */ + protected KeyDeserializer _keyDeserializer; + + /** + * Value deserializer. + */ + protected JsonDeserializer _valueDeserializer; + + /** + * If value instances have polymorphic type information, this + * is the type deserializer that can handle it + */ + protected final TypeDeserializer _typeDeserializerForValue; + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + protected GuavaMapDeserializer(MapType type, KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + _mapType = type; + _keyDeserializer = keyDeser; + _typeDeserializerForValue = typeDeser; + _valueDeserializer = deser; + } + + /** + * Overridable fluent factory method used for creating contextual + * instances. + */ + public abstract GuavaMapDeserializer withResolved(KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer valueDeser); + + /* + /********************************************************** + /* Validation, post-processing + /********************************************************** + */ + + /** + * Method called to finalize setup of this deserializer, + * after deserializer itself has been registered. This + * is needed to handle recursive and transitive dependencies. + */ + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException + { + KeyDeserializer keyDeser = _keyDeserializer; + JsonDeserializer deser = _valueDeserializer; + TypeDeserializer typeDeser = _typeDeserializerForValue; + // Do we need any contextualization? + if ((keyDeser != null) && (deser != null) && (typeDeser == null)) { // nope + return this; + } + if (keyDeser == null) { + keyDeser = ctxt.findKeyDeserializer(_mapType.getKeyType(), property); + } + if (deser == null) { + deser = ctxt.findContextualValueDeserializer(_mapType.getContentType(), property); + } + if (typeDeser != null) { + typeDeser = typeDeser.forProperty(property); + } + return withResolved(keyDeser, typeDeser, deser); + } + + /* + /********************************************************** + /* Deserialization interface + /********************************************************** + */ + + /** + * Base implementation that does not assume specific type + * inclusion mechanism. Sub-classes are expected to override + * this method if they are to handle type information. + */ + @Override + public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) + throws IOException, JsonProcessingException + { + // note: call "...FromObject" because expected output structure + // for value is JSON Object (regardless of contortions used for type id) + return typeDeserializer.deserializeTypedFromObject(jp, ctxt); + } + + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + // Ok: must point to START_OBJECT or FIELD_NAME + JsonToken t = jp.getCurrentToken(); + if (t == JsonToken.START_OBJECT) { // If START_OBJECT, move to next; may also be END_OBJECT + t = jp.nextToken(); + } + if (t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { + throw ctxt.mappingException(_mapType.getRawClass()); + } + return _deserializeEntries(jp, ctxt); + } + + /* + /********************************************************************** + /* Abstract methods for impl classes + /********************************************************************** + */ + + protected abstract T _deserializeEntries(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException; +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMultisetDeserializer.java new file mode 100644 index 00000000..df340752 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaMultisetDeserializer.java @@ -0,0 +1,70 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.Multiset; + +abstract class GuavaMultisetDeserializer> + extends GuavaCollectionDeserializer +{ + private static final long serialVersionUID = 1L; + + GuavaMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + protected abstract T createMultiset(); + + @Override + protected T _deserializeContents(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + JsonDeserializer valueDes = _valueDeserializer; + JsonToken t; + final TypeDeserializer typeDeser = _typeDeserializerForValue; + T set = createMultiset(); + + while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { + Object value; + + if (t == JsonToken.VALUE_NULL) { + value = null; + } else if (typeDeser == null) { + value = valueDes.deserialize(jp, ctxt); + } else { + value = valueDes.deserializeWithType(jp, ctxt, typeDeser); + } + set.add(value); + } + return set; + } + + @Override + protected T _deserializeFromSingleValue(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException + { + JsonDeserializer valueDes = _valueDeserializer; + final TypeDeserializer typeDeser = _typeDeserializerForValue; + JsonToken t = jp.getCurrentToken(); + + Object value; + + if (t == JsonToken.VALUE_NULL) { + value = null; + } else if (typeDeser == null) { + value = valueDes.deserialize(jp, ctxt); + } else { + value = valueDes.deserializeWithType(jp, ctxt, typeDeser); + } + T result = createMultiset(); + result.add(value); + return result; + } + +} \ No newline at end of file diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaOptionalDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaOptionalDeserializer.java new file mode 100644 index 00000000..79373e15 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/GuavaOptionalDeserializer.java @@ -0,0 +1,145 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import com.google.common.base.Optional; + +public class GuavaOptionalDeserializer + extends StdDeserializer> + implements ContextualDeserializer +{ + private static final long serialVersionUID = 1L; + + /** + * Full type of `Optional` property. + */ + protected final JavaType _fullType; + + protected final JsonDeserializer _valueDeserializer; + + protected final TypeDeserializer _valueTypeDeserializer; + + public GuavaOptionalDeserializer(JavaType fullType, + TypeDeserializer typeDeser, JsonDeserializer valueDeser) + { + super(fullType); + _fullType = fullType; + _valueTypeDeserializer = typeDeser; + _valueDeserializer = valueDeser; + } + + @Override + public JavaType getValueType() { return _fullType; } + + @Override + public Optional getNullValue(DeserializationContext ctxt) { + return Optional.absent(); + } + + @Override + @Deprecated // since 2.6; remove from 2.8 + public Optional getNullValue() { + return Optional.absent(); + } + + /** + * Overridable fluent factory method used for creating contextual + * instances. + */ + protected GuavaOptionalDeserializer withResolved(JavaType fullType, + TypeDeserializer typeDeser, JsonDeserializer valueDeser) + { + if ((_fullType == fullType) + && (valueDeser == _valueDeserializer) && (typeDeser == _valueTypeDeserializer)) { + return this; + } + return new GuavaOptionalDeserializer(_fullType, typeDeser, valueDeser); + } + + /* + /********************************************************** + /* Validation, post-processing + /********************************************************** + */ + + /** + * Method called to finalize setup of this deserializer, + * after deserializer itself has been registered. This + * is needed to handle recursive and transitive dependencies. + */ + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException + { + JsonDeserializer deser = _valueDeserializer; + TypeDeserializer typeDeser = _valueTypeDeserializer; + JavaType fullType = _fullType; + + if (deser == null) { + // 08-Oct-2015, tatu: need to allow type override, if any + if (property != null) { + AnnotationIntrospector intr = ctxt.getAnnotationIntrospector(); + AnnotatedMember member = property.getMember(); + if ((intr != null) && (member != null)) { + fullType = intr.refineDeserializationType(ctxt.getConfig(), member, fullType); + } + } + JavaType refdType = fullType.getContentType(); + if (refdType == null) { + refdType = TypeFactory.unknownType(); + } + deser = ctxt.findContextualValueDeserializer(refdType, property); + } else { // otherwise directly assigned, probably not contextual yet: + JavaType refdType = fullType.getContentType(); + if (refdType == null) { + refdType = TypeFactory.unknownType(); + } + deser = ctxt.handleSecondaryContextualization(deser, property, refdType); + } + if (typeDeser != null) { + typeDeser = typeDeser.forProperty(property); + } + return withResolved(fullType, typeDeser, deser); + } + + @Override + public Optional deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + Object refd = (_valueTypeDeserializer == null) + ? _valueDeserializer.deserialize(p, ctxt) + : _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer); + return Optional.fromNullable(refd); + } + + @Override + public Optional deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) + throws IOException + { + final JsonToken t = p.getCurrentToken(); + if (t == JsonToken.VALUE_NULL) { + return getNullValue(); + } + // 03-Nov-2013, tatu: This gets rather tricky with "natural" types + // (String, Integer, Boolean), which do NOT include type information. + // These might actually be handled ok except that nominal type here + // is `Optional`, so special handling is not invoked; instead, need + // to do a work-around here. + // 22-Oct-2015, tatu: Most likely this is actually wrong, result of incorrewct + // serialization (up to 2.6, was omitting necessary type info after all); + // but safest to leave in place for now + if (t != null && t.isScalarValue()) { + return deserialize(p, ctxt); + } + return (Optional) typeDeserializer.deserializeTypedFromAny(p, ctxt); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashCodeDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashCodeDeserializer.java new file mode 100644 index 00000000..9410c336 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashCodeDeserializer.java @@ -0,0 +1,23 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; +import java.util.Locale; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.common.hash.HashCode; + +public class HashCodeDeserializer extends FromStringDeserializer +{ + private static final long serialVersionUID = 1L; + + public final static HashCodeDeserializer std = new HashCodeDeserializer(); + + public HashCodeDeserializer() { super(HashCode.class); } + + @Override + protected HashCode _deserialize(String value, DeserializationContext ctxt) + throws IOException { + return HashCode.fromString(value.toLowerCase(Locale.ENGLISH)); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashMultisetDeserializer.java new file mode 100644 index 00000000..212c6b1f --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HashMultisetDeserializer.java @@ -0,0 +1,31 @@ +package com.fasterxml.jackson.datatype.guava.deser; + + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.HashMultiset; + +public class HashMultisetDeserializer + extends GuavaMultisetDeserializer> +{ + private static final long serialVersionUID = 1L; + + public HashMultisetDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + super(type, typeDeser, deser); + } + + @Override + public HashMultisetDeserializer withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new HashMultisetDeserializer(_containerType, + typeDeser, valueDeser); + } + + @Override + protected HashMultiset createMultiset() { + return HashMultiset. create(); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HostAndPortDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HostAndPortDeserializer.java new file mode 100644 index 00000000..e1d8f777 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/HostAndPortDeserializer.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; +import com.google.common.net.HostAndPort; + +public class HostAndPortDeserializer extends FromStringDeserializer +{ + private static final long serialVersionUID = 1L; + + public final static HostAndPortDeserializer std = new HostAndPortDeserializer(); + + public HostAndPortDeserializer() { super(HostAndPort.class); } + + @Override + public HostAndPort deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException + { + // Need to override this method, which otherwise would work just fine, + // since we have legacy JSON Object format to support too: + if (jp.getCurrentToken() == JsonToken.START_OBJECT) { // old style + JsonNode root = jp.readValueAsTree(); + String host = root.path("hostText").asText(); + JsonNode n = root.get("port"); + if (n == null) { + return HostAndPort.fromString(host); + } + return HostAndPort.fromParts(host, n.asInt()); + } + return super.deserialize(jp, ctxt); + } + + @Override + protected HostAndPort _deserialize(String value, DeserializationContext ctxt) + throws IOException { + return HostAndPort.fromString(value); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableBiMapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableBiMapDeserializer.java new file mode 100644 index 00000000..c6a7f515 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableBiMapDeserializer.java @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap.Builder; + +public class ImmutableBiMapDeserializer extends GuavaImmutableMapDeserializer> { + public ImmutableBiMapDeserializer(MapType type, KeyDeserializer keyDeser, TypeDeserializer typeDeser, + JsonDeserializer deser) { + super(type, keyDeser, typeDeser, deser); + } + + @Override + protected Builder createBuilder() { + return ImmutableBiMap.builder(); + } + + @Override + public GuavaMapDeserializer> withResolved(KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer valueDeser) { + return new ImmutableBiMapDeserializer(_mapType, keyDeser, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableListDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableListDeserializer.java new file mode 100644 index 00000000..51a3e5b9 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableListDeserializer.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.google.common.collect.ImmutableList; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; + +public class ImmutableListDeserializer extends + GuavaImmutableCollectionDeserializer> +{ + private static final long serialVersionUID = 1L; + + public ImmutableListDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + @Override + public ImmutableListDeserializer withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new ImmutableListDeserializer(_containerType, typeDeser, + valueDeser); + } + + /* + /********************************************************** + /* Deserialization + /********************************************************** + */ + + @Override + protected ImmutableList.Builder createBuilder() { + ImmutableList.Builder builder = ImmutableList.builder(); + return builder; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMapDeserializer.java new file mode 100644 index 00000000..6421b492 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMapDeserializer.java @@ -0,0 +1,31 @@ +package com.fasterxml.jackson.datatype.guava.deser; + + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.google.common.collect.ImmutableMap; + +public class ImmutableMapDeserializer + extends GuavaImmutableMapDeserializer> +{ + public ImmutableMapDeserializer(MapType type, KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + super(type, keyDeser, typeDeser, deser); + } + + @Override + public ImmutableMapDeserializer withResolved(KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer valueDeser) { + return new ImmutableMapDeserializer(_mapType, keyDeser, + typeDeser, valueDeser); + } + + @Override + protected ImmutableMap.Builder createBuilder() { + return ImmutableMap.builder(); + } + +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMultisetDeserializer.java new file mode 100644 index 00000000..c488ed88 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableMultisetDeserializer.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.ImmutableCollection.Builder; +import com.google.common.collect.ImmutableMultiset; + +public class ImmutableMultisetDeserializer extends GuavaImmutableCollectionDeserializer> +{ + private static final long serialVersionUID = 1L; + + public ImmutableMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + @Override + protected Builder createBuilder() { + return ImmutableMultiset.builder(); + } + + @Override + public GuavaCollectionDeserializer> withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new ImmutableMultisetDeserializer(_containerType, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSetDeserializer.java new file mode 100644 index 00000000..132200d1 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSetDeserializer.java @@ -0,0 +1,30 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.ImmutableCollection.Builder; +import com.google.common.collect.ImmutableSet; + +public class ImmutableSetDeserializer extends GuavaImmutableCollectionDeserializer> +{ + private static final long serialVersionUID = 1L; + + public ImmutableSetDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + super(type, typeDeser, deser); + } + + @Override + public ImmutableSetDeserializer withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new ImmutableSetDeserializer(_containerType, + typeDeser, valueDeser); + } + + @Override + protected Builder createBuilder() { + return ImmutableSet.builder(); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMapDeserializer.java new file mode 100644 index 00000000..179e1de6 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMapDeserializer.java @@ -0,0 +1,36 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ImmutableSortedMap; + +public class ImmutableSortedMapDeserializer extends GuavaImmutableMapDeserializer> { + + public ImmutableSortedMapDeserializer(MapType type, KeyDeserializer keyDeser, TypeDeserializer typeDeser, + JsonDeserializer deser) { + super(type, keyDeser, typeDeser, deser); + } + + @SuppressWarnings("unchecked") + @Override + protected Builder createBuilder() { + /* + * Not quite sure what to do with sorting/ordering; may require better + * support either via annotations, or via custom serialization (bean + * style that includes ordering aspects) + */ + @SuppressWarnings("rawtypes") + ImmutableSortedMap.Builder naturalOrder = ImmutableSortedMap.naturalOrder(); + ImmutableSortedMap.Builder builder = (ImmutableSortedMap.Builder) naturalOrder; + return builder; + } + + @Override + public GuavaMapDeserializer> withResolved(KeyDeserializer keyDeser, + TypeDeserializer typeDeser, JsonDeserializer valueDeser) { + return new ImmutableSortedMapDeserializer(_mapType, keyDeser, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMultisetDeserializer.java new file mode 100644 index 00000000..12949186 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedMultisetDeserializer.java @@ -0,0 +1,30 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.ImmutableCollection.Builder; +import com.google.common.collect.ImmutableSortedMultiset; + +public class ImmutableSortedMultisetDeserializer extends GuavaImmutableCollectionDeserializer> +{ + private static final long serialVersionUID = 1L; + + public ImmutableSortedMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + @Override + protected Builder createBuilder() { + /* This is suboptimal. See the considerations in ImmutableSortedSetDeserializer. */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + Builder builder = (Builder) ImmutableSortedMultiset.naturalOrder(); + return builder; + } + + @Override + public GuavaCollectionDeserializer> withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new ImmutableSortedMultisetDeserializer(_containerType, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedSetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedSetDeserializer.java new file mode 100644 index 00000000..ff46bb4f --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/ImmutableSortedSetDeserializer.java @@ -0,0 +1,38 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.ImmutableCollection.Builder; +import com.google.common.collect.ImmutableSortedSet; + +public class ImmutableSortedSetDeserializer extends GuavaImmutableCollectionDeserializer> +{ + private static final long serialVersionUID = 1L; + + public ImmutableSortedSetDeserializer(CollectionType type, + TypeDeserializer typeDeser, JsonDeserializer deser) + { + super(type, typeDeser, deser); + } + + @Override + public ImmutableSortedSetDeserializer withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new ImmutableSortedSetDeserializer(_containerType, + typeDeser, valueDeser); + } + + @SuppressWarnings("unchecked") + @Override + protected Builder createBuilder() { + /* Not quite sure what to do with sorting/ordering; may require better support either + * via annotations, or via custom serialization (bean style that includes ordering + * aspects) + */ + @SuppressWarnings("rawtypes") + ImmutableSortedSet.Builder builderComp = ImmutableSortedSet. naturalOrder(); + ImmutableSortedSet.Builder builder = (ImmutableSortedSet.Builder) builderComp; + return builder; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/InternetDomainNameDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/InternetDomainNameDeserializer.java new file mode 100644 index 00000000..f8ed124b --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/InternetDomainNameDeserializer.java @@ -0,0 +1,23 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.google.common.net.InternetDomainName; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer; + +public class InternetDomainNameDeserializer extends FromStringDeserializer +{ + private static final long serialVersionUID = 1L; + + public final static InternetDomainNameDeserializer std = new InternetDomainNameDeserializer(); + + public InternetDomainNameDeserializer() { super(InternetDomainName.class); } + + @Override + protected InternetDomainName _deserialize(String value, DeserializationContext ctxt) + throws IOException { + return InternetDomainName.from(value); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/LinkedHashMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/LinkedHashMultisetDeserializer.java new file mode 100644 index 00000000..7306ca6a --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/LinkedHashMultisetDeserializer.java @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.LinkedHashMultiset; + +public class LinkedHashMultisetDeserializer extends GuavaMultisetDeserializer> +{ + private static final long serialVersionUID = 1L; + + public LinkedHashMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + @Override + protected LinkedHashMultiset createMultiset() { + return LinkedHashMultiset.create(); + } + + @Override + public GuavaCollectionDeserializer> withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new LinkedHashMultisetDeserializer(_containerType, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/RangeDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/RangeDeserializer.java new file mode 100644 index 00000000..e2d3bae5 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/RangeDeserializer.java @@ -0,0 +1,197 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory; + +/** + * Jackson deserializer for a Guava {@link Range}. + *

+ * TODO: I think it would make sense to reimplement this deserializer to + * use Delegating Deserializer, using a POJO as an intermediate form (properties + * could be of type {@link java.lang.Object}) + * This would also also simplify the implementation a bit. + */ +public class RangeDeserializer + extends StdDeserializer> + implements ContextualDeserializer +{ + private static final long serialVersionUID = 1L; + + protected final JavaType _rangeType; + + protected final JsonDeserializer _endpointDeserializer; + + private BoundType _defaultBoundType; + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + /** + * @deprecated Since 2.7 + */ + @Deprecated // since 2.7 + public RangeDeserializer(JavaType rangeType) { + this(null, rangeType); + } + + public RangeDeserializer(BoundType defaultBoundType, JavaType rangeType) { + this(rangeType, null); + _defaultBoundType = defaultBoundType; + } + + @SuppressWarnings("unchecked") + public RangeDeserializer(JavaType rangeType, JsonDeserializer endpointDeser) + { + super(rangeType); + _rangeType = rangeType; + _endpointDeserializer = (JsonDeserializer) endpointDeser; + } + + @SuppressWarnings("unchecked") + public RangeDeserializer(JavaType rangeType, JsonDeserializer endpointDeser, BoundType defaultBoundType) + { + super(rangeType); + _rangeType = rangeType; + _endpointDeserializer = (JsonDeserializer) endpointDeser; + _defaultBoundType = defaultBoundType; + } + + @Override + public JavaType getValueType() { return _rangeType; } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException + { + if (_endpointDeserializer == null) { + JavaType endpointType = _rangeType.containedType(0); + if (endpointType == null) { // should this ever occur? + endpointType = TypeFactory.unknownType(); + } + JsonDeserializer deser = ctxt.findContextualValueDeserializer(endpointType, property); + return new RangeDeserializer(_rangeType, deser, _defaultBoundType); + } + return this; + } + + /* + /********************************************************** + /* Actual deserialization + /********************************************************** + */ + + @Override + public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) + throws IOException + { + return typeDeserializer.deserializeTypedFromObject(jp, ctxt); + } + + @Override + public Range deserialize(JsonParser parser, DeserializationContext context) + throws IOException + { + // NOTE: either START_OBJECT _or_ FIELD_NAME fine; latter for polymorphic cases + JsonToken t = parser.getCurrentToken(); + if (t == JsonToken.START_OBJECT) { + t = parser.nextToken(); + } + + Comparable lowerEndpoint = null; + Comparable upperEndpoint = null; + BoundType lowerBoundType = _defaultBoundType; + BoundType upperBoundType = _defaultBoundType; + + for (; t != JsonToken.END_OBJECT; t = parser.nextToken()) { + expect(parser, JsonToken.FIELD_NAME, t); + String fieldName = parser.getCurrentName(); + try { + if (fieldName.equals("lowerEndpoint")) { + parser.nextToken(); + lowerEndpoint = deserializeEndpoint(parser, context); + } else if (fieldName.equals("upperEndpoint")) { + parser.nextToken(); + upperEndpoint = deserializeEndpoint(parser, context); + } else if (fieldName.equals("lowerBoundType")) { + parser.nextToken(); + lowerBoundType = deserializeBoundType(parser); + } else if (fieldName.equals("upperBoundType")) { + parser.nextToken(); + upperBoundType = deserializeBoundType(parser); + } else { + throw context.mappingException("Unexpected Range field: " + fieldName); + } + } catch (IllegalStateException e) { + throw JsonMappingException.from(parser, e.getMessage()); + } + } + try { + if ((lowerEndpoint != null) && (upperEndpoint != null)) { + Preconditions.checkState(lowerEndpoint.getClass() == upperEndpoint.getClass(), + "Endpoint types are not the same - 'lowerEndpoint' deserialized to [%s], and 'upperEndpoint' deserialized to [%s].", + lowerEndpoint.getClass().getName(), + upperEndpoint.getClass().getName()); + Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'"); + Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'"); + return RangeFactory.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType); + } + if (lowerEndpoint != null) { + Preconditions.checkState(lowerBoundType != null, "'lowerEndpoint' field found, but not 'lowerBoundType'"); + return RangeFactory.downTo(lowerEndpoint, lowerBoundType); + } + if (upperEndpoint != null) { + Preconditions.checkState(upperBoundType != null, "'upperEndpoint' field found, but not 'upperBoundType'"); + return RangeFactory.upTo(upperEndpoint, upperBoundType); + } + return RangeFactory.all(); + } catch (IllegalStateException e) { + throw JsonMappingException.from(parser, e.getMessage()); + } + } + + private BoundType deserializeBoundType(JsonParser parser) throws IOException + { + expect(parser, JsonToken.VALUE_STRING, parser.getCurrentToken()); + String name = parser.getText(); + try { + return BoundType.valueOf(name); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("[" + name + "] is not a valid BoundType name."); + } + } + + private Comparable deserializeEndpoint(JsonParser parser, DeserializationContext context) throws IOException + { + Object obj = _endpointDeserializer.deserialize(parser, context); + if (!(obj instanceof Comparable)) { + throw context.mappingException(String.format( + "Field [%s] deserialized to [%s], which does not implement Comparable.", + parser.getCurrentName(), obj.getClass().getName())); + } + return (Comparable) obj; + } + + private void expect(JsonParser p, JsonToken expected, JsonToken actual) throws JsonMappingException + { + if (actual != expected) { + throw JsonMappingException.from(p, "Expecting " + expected + ", found " + actual); + } + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/TreeMultisetDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/TreeMultisetDeserializer.java new file mode 100644 index 00000000..ff0ab476 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/TreeMultisetDeserializer.java @@ -0,0 +1,29 @@ +package com.fasterxml.jackson.datatype.guava.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.google.common.collect.TreeMultiset; + +public class TreeMultisetDeserializer extends GuavaMultisetDeserializer> +{ + private static final long serialVersionUID = 1L; + + public TreeMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) { + super(type, typeDeser, deser); + } + + @SuppressWarnings("unchecked") + @Override + protected TreeMultiset createMultiset() { + @SuppressWarnings("rawtypes") + TreeMultiset naturalOrder = TreeMultiset. create(); + return (TreeMultiset) naturalOrder; + } + + @Override + public GuavaCollectionDeserializer> withResolved(TypeDeserializer typeDeser, + JsonDeserializer valueDeser) { + return new TreeMultisetDeserializer(_containerType, typeDeser, valueDeser); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/GuavaMultimapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/GuavaMultimapDeserializer.java new file mode 100644 index 00000000..53cb4f32 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/GuavaMultimapDeserializer.java @@ -0,0 +1,183 @@ +package com.fasterxml.jackson.datatype.guava.deser.multimap; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +/** + * @author mvolkhart + */ +public abstract class GuavaMultimapDeserializer> extends JsonDeserializer implements ContextualDeserializer { + + private static final List METHOD_NAMES = ImmutableList.of("copyOf", "create"); + private final MapLikeType type; + private final KeyDeserializer keyDeserializer; + private final TypeDeserializer elementTypeDeserializer; + private final JsonDeserializer elementDeserializer; + /** + * Since we have to use a method to transform from a known multi-map type into actual one, we'll + * resolve method just once, use it. Note that if this is set to null, we can just construct a + * {@link com.google.common.collect.LinkedListMultimap} instance and be done with it. + */ + private final Method creatorMethod; + + public GuavaMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + this(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, + findTransformer(type.getRawClass())); + } + + public GuavaMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + Method creatorMethod) { + this.type = type; + this.keyDeserializer = keyDeserializer; + this.elementTypeDeserializer = elementTypeDeserializer; + this.elementDeserializer = elementDeserializer; + this.creatorMethod = creatorMethod; + } + + private static Method findTransformer(Class rawType) { + // Very first thing: if it's a "standard multi-map type", can avoid copying + if (rawType == LinkedListMultimap.class || rawType == ListMultimap.class || rawType == + Multimap.class) { + return null; + } + + // First, check type itself for matching methods + for (String methodName : METHOD_NAMES) { + try { + Method m = rawType.getMethod(methodName, Multimap.class); + if (m != null) { + return m; + } + } catch (NoSuchMethodException e) { + } + // pass SecurityExceptions as-is: + // } catch (SecurityException e) { } + } + + // If not working, possibly super types too (should we?) + for (String methodName : METHOD_NAMES) { + try { + Method m = rawType.getMethod(methodName, Multimap.class); + if (m != null) { + return m; + } + } catch (NoSuchMethodException e) { + } + // pass SecurityExceptions as-is: + // } catch (SecurityException e) { } + } + + return null; + } + + protected abstract T createMultimap(); + + /** + * We need to use this method to properly handle possible contextual variants of key and value + * deserializers, as well as type deserializers. + */ + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, + BeanProperty property) throws JsonMappingException { + KeyDeserializer kd = keyDeserializer; + if (kd == null) { + kd = ctxt.findKeyDeserializer(type.getKeyType(), property); + } + JsonDeserializer ed = elementDeserializer; + if (ed == null) { + ed = ctxt.findContextualValueDeserializer(type.getContentType(), property); + } + // Type deserializer is slightly different; must be passed, but needs to become contextual: + TypeDeserializer etd = elementTypeDeserializer; + if (etd != null && property != null) { + etd = etd.forProperty(property); + } + return (_createContextual(type, kd, etd, ed, creatorMethod)); + } + + protected abstract JsonDeserializer _createContextual(MapLikeType t, + KeyDeserializer kd, TypeDeserializer typeDeserializer, + JsonDeserializer ed, Method method); + + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + + T multimap = createMultimap(); + + expect(jp, JsonToken.START_OBJECT); + + while (jp.nextToken() != JsonToken.END_OBJECT) { + final Object key; + if (keyDeserializer != null) { + key = keyDeserializer.deserializeKey(jp.getCurrentName(), ctxt); + } else { + key = jp.getCurrentName(); + } + + jp.nextToken(); + expect(jp, JsonToken.START_ARRAY); + + while (jp.nextToken() != JsonToken.END_ARRAY) { + final Object value; + if (jp.getCurrentToken() == JsonToken.VALUE_NULL) { + value = null; + } else if (elementTypeDeserializer != null) { + value = elementDeserializer.deserializeWithType(jp, ctxt, + elementTypeDeserializer); + } else { + value = elementDeserializer.deserialize(jp, ctxt); + } + multimap.put(key, value); + } + } + if (creatorMethod == null) { + return multimap; + } + try { + @SuppressWarnings("unchecked") T map = (T) creatorMethod.invoke(null, multimap); + return map; + } catch (InvocationTargetException e) { + throw new JsonMappingException(jp, "Could not map to " + type, _peel(e)); + } catch (IllegalArgumentException e) { + throw new JsonMappingException(jp, "Could not map to " + type, _peel(e)); + } catch (IllegalAccessException e) { + throw new JsonMappingException(jp, "Could not map to " + type, _peel(e)); + } + } + + private void expect(JsonParser jp, JsonToken token) throws IOException { + if (jp.getCurrentToken() != token) { + throw new JsonMappingException(jp, "Expecting " + token + ", found " + jp.getCurrentToken(), + jp.getCurrentLocation()); + } + } + + private Throwable _peel(Throwable t) { + while (t.getCause() != null) { + t = t.getCause(); + } + return t; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/ArrayListMultimapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/ArrayListMultimapDeserializer.java new file mode 100644 index 00000000..cdf673b9 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/ArrayListMultimapDeserializer.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.guava.deser.multimap.list; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.datatype.guava.deser.multimap.GuavaMultimapDeserializer; +import com.google.common.collect.ArrayListMultimap; + +import java.lang.reflect.Method; + +/** + * Provides deserialization for the Guava ArrayListMultimap class. + * + * @author mvolkhart + */ +public class ArrayListMultimapDeserializer extends GuavaMultimapDeserializer> { + + public ArrayListMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + public ArrayListMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + Method creatorMethod) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, creatorMethod); + } + + @Override + protected ArrayListMultimap createMultimap() { + return ArrayListMultimap.create(); + } + + @Override + protected JsonDeserializer _createContextual(MapLikeType type, + KeyDeserializer keyDeserializer, TypeDeserializer typeDeserializer, + JsonDeserializer elementDeserializer, Method method) { + return new ArrayListMultimapDeserializer(type, keyDeserializer, typeDeserializer, + elementDeserializer, method); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/LinkedListMultimapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/LinkedListMultimapDeserializer.java new file mode 100644 index 00000000..263f6fea --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/list/LinkedListMultimapDeserializer.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.guava.deser.multimap.list; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.datatype.guava.deser.multimap.GuavaMultimapDeserializer; +import com.google.common.collect.LinkedListMultimap; + +import java.lang.reflect.Method; + +/** + * Provides deserialization for the Guava LinkedListMultimap class. + * + * @author mvolkhart + */ +public class LinkedListMultimapDeserializer + extends GuavaMultimapDeserializer> +{ + public LinkedListMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + public LinkedListMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + Method creatorMethod) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, creatorMethod); + } + + @Override + protected LinkedListMultimap createMultimap() { + return LinkedListMultimap.create(); + } + + @Override + protected JsonDeserializer _createContextual(MapLikeType type, + KeyDeserializer keyDeserializer, TypeDeserializer typeDeserializer, + JsonDeserializer elementDeserializer, Method method) { + return new LinkedListMultimapDeserializer(type, keyDeserializer, typeDeserializer, + elementDeserializer, method); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/HashMultimapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/HashMultimapDeserializer.java new file mode 100644 index 00000000..75693211 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/HashMultimapDeserializer.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.guava.deser.multimap.set; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.datatype.guava.deser.multimap.GuavaMultimapDeserializer; +import com.google.common.collect.HashMultimap; + +import java.lang.reflect.Method; + +/** + * Provides deserialization for the Guava HashMultimap class. + * + * @author mvolkhart + */ +public class HashMultimapDeserializer extends GuavaMultimapDeserializer> { + + public HashMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + public HashMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + Method creatorMethod) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, creatorMethod); + } + + @Override + protected HashMultimap createMultimap() { + return HashMultimap.create(); + } + + @Override + protected JsonDeserializer _createContextual(MapLikeType type, + KeyDeserializer keyDeserializer, TypeDeserializer typeDeserializer, + JsonDeserializer elementDeserializer, Method method) { + return new HashMultimapDeserializer(type, keyDeserializer, typeDeserializer, + elementDeserializer, method); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/LinkedHashMultimapDeserializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/LinkedHashMultimapDeserializer.java new file mode 100644 index 00000000..fc442acc --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/multimap/set/LinkedHashMultimapDeserializer.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.guava.deser.multimap.set; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.MapLikeType; +import com.fasterxml.jackson.datatype.guava.deser.multimap.GuavaMultimapDeserializer; +import com.google.common.collect.LinkedHashMultimap; + +import java.lang.reflect.Method; + +/** + * Provides deserialization for the Guava LinkedHashMultimap class. + * + * @author mvolkhart + */ +public class LinkedHashMultimapDeserializer extends + GuavaMultimapDeserializer> { + + public LinkedHashMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer); + } + + public LinkedHashMultimapDeserializer(MapLikeType type, KeyDeserializer keyDeserializer, + TypeDeserializer elementTypeDeserializer, JsonDeserializer elementDeserializer, + Method creatorMethod) { + super(type, keyDeserializer, elementTypeDeserializer, elementDeserializer, creatorMethod); + } + + @Override + protected LinkedHashMultimap createMultimap() { + return LinkedHashMultimap.create(); + } + + @Override + protected JsonDeserializer _createContextual(MapLikeType type, + KeyDeserializer keyDeserializer, TypeDeserializer typeDeserializer, + JsonDeserializer elementDeserializer, Method method) { + return new LinkedHashMultimapDeserializer(type, keyDeserializer, typeDeserializer, + elementDeserializer, method); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/util/RangeFactory.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/util/RangeFactory.java new file mode 100644 index 00000000..0d7c54d8 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/deser/util/RangeFactory.java @@ -0,0 +1,181 @@ + +package com.fasterxml.jackson.datatype.guava.deser.util; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Lists; +import com.google.common.collect.Range; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * A factory for creating Guava {@link Range}s that is compatible with Guava 10 and later. + * + * If Guava 10, 11, 12, or 13 is being used, the factory methods in the com.google.common.collect.Ranges class (a beta + * class that was removed in Guava 14) are used to reflectively instantiate Ranges. If Guava 14 or later is being used, + * the factory methods in the Range class itself (added in Guava 14) are used to instantiate Ranges. + */ +public class RangeFactory +{ + private static final String LEGACY_RANGES_CLASS_NAME = "com.google.common.collect.Ranges"; + + private static final String LEGACY_RANGE_METHOD_NAME = "range"; + private static final String LEGACY_DOWN_TO_METHOD_NAME = "downTo"; + private static final String LEGACY_UP_TO_METHOD_NAME = "upTo"; + private static final String LEGACY_ALL_METHOD_NAME = "all"; + + private static Method legacyRangeMethod; + private static Method legacyDownToMethod; + private static Method legacyUpToMethod; + private static Method legacyAllMethod; + + static + { + initLegacyRangeFactoryMethods(); + } + + private static void initLegacyRangeFactoryMethods() + { + try { + Class rangesClass = Class.forName(LEGACY_RANGES_CLASS_NAME); + legacyRangeMethod = findMethod(rangesClass, LEGACY_RANGE_METHOD_NAME, Comparable.class, BoundType.class, + Comparable.class, BoundType.class); + legacyDownToMethod = findMethod(rangesClass, LEGACY_DOWN_TO_METHOD_NAME, Comparable.class, BoundType.class); + legacyUpToMethod = findMethod(rangesClass, LEGACY_UP_TO_METHOD_NAME, Comparable.class, BoundType.class); + legacyAllMethod = findMethod(rangesClass, LEGACY_ALL_METHOD_NAME); + } catch (ClassNotFoundException e) { + // ignore + } + } + + // returns null if the method is not found + private static Method findMethod(Class clazz, String methodName, Class ... paramTypes) + { + try { + return clazz.getMethod(methodName, paramTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + + public static > Range open(C lowerEndpoint, C upperEndpoint) + { + return range(lowerEndpoint, BoundType.OPEN, upperEndpoint, BoundType.OPEN); + } + + public static > Range openClosed(C lowerEndpoint, C upperEndpoint) + { + return range(lowerEndpoint, BoundType.OPEN, upperEndpoint, BoundType.CLOSED); + } + + public static > Range closedOpen(C lowerEndpoint, C upperEndpoint) + { + return range(lowerEndpoint, BoundType.CLOSED, upperEndpoint, BoundType.OPEN); + } + + public static > Range closed(C lowerEndpoint, C upperEndpoint) + { + return range(lowerEndpoint, BoundType.CLOSED, upperEndpoint, BoundType.CLOSED); + } + + public static > Range range(final C lowerEndpoint, final BoundType lowerBoundType, + final C upperEndpoint, final BoundType upperBoundType) + { + return createRange(new Callable>() { + @Override + public Range call() throws Exception { + return Range.range(lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType); + } + }, legacyRangeMethod, lowerEndpoint, lowerBoundType, upperEndpoint, upperBoundType); + } + + public static > Range greaterThan(C lowerEndpoint) + { + return downTo(lowerEndpoint, BoundType.OPEN); + } + + public static > Range atLeast(C lowerEndpoint) + { + return downTo(lowerEndpoint, BoundType.CLOSED); + } + + public static > Range downTo(final C lowerEndpoint, final BoundType lowerBoundType) + { + return createRange(new Callable>() { + @Override + public Range call() throws Exception { + return Range.downTo(lowerEndpoint, lowerBoundType); + } + }, legacyDownToMethod, lowerEndpoint, lowerBoundType); + } + + public static > Range lessThan(C upperEndpoint) + { + return upTo(upperEndpoint, BoundType.OPEN); + } + + public static > Range atMost(C upperEndpoint) + { + return upTo(upperEndpoint, BoundType.CLOSED); + } + + public static > Range upTo(final C upperEndpoint, final BoundType upperBoundType) + { + return createRange(new Callable>() { + @Override + public Range call() throws Exception { + return Range.upTo(upperEndpoint, upperBoundType); + } + }, legacyUpToMethod, upperEndpoint, upperBoundType); + } + + public static > Range all() + { + return createRange(new Callable>() { + @Override + public Range call() throws Exception { + return Range.all(); + } + }, legacyAllMethod); + } + + public static > Range singleton(final C value) + { + return createRange(new Callable>() { + @Override + public Range call() throws Exception { + return Range.singleton(value); + } + }, legacyRangeMethod, value, BoundType.CLOSED, value, BoundType.CLOSED); + } + + private static > Range createRange(Callable> rangeCallable, Method legacyRangeFactoryMethod, Object ... params) + { + try { + return rangeCallable.call(); + } catch (NoSuchMethodError noSuchMethodError) { + if (legacyRangeFactoryMethod != null) { + return invokeLegacyRangeFactoryMethod(legacyRangeFactoryMethod, params); + } else { + throw noSuchMethodError; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private static > Range invokeLegacyRangeFactoryMethod(Method method, Object... params) + { + try { + //noinspection unchecked + return (Range) method.invoke(null, params); + } catch (Exception e) { + throw new RuntimeException("Failed to invoke legacy Range factory method [" + method.getName() + + "] with params " + Lists.newArrayList(params) + ".", e); + } + } + + // prevent instantiation + private RangeFactory() { } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaBeanSerializerModifier.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaBeanSerializerModifier.java new file mode 100644 index 00000000..abc3a2a6 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaBeanSerializerModifier.java @@ -0,0 +1,30 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.google.common.base.Optional; + +import java.util.List; + +/** + * {@link BeanSerializerModifier} needed to sneak in handler to exclude "absent" + * optional values iff handling of "absent as nulls" is enabled. + */ +public class GuavaBeanSerializerModifier extends BeanSerializerModifier +{ + @Override + public List changeProperties(SerializationConfig config, + BeanDescription beanDesc, + List beanProperties) + { + for (int i = 0; i < beanProperties.size(); ++i) { + final BeanPropertyWriter writer = beanProperties.get(i); + if (Optional.class.isAssignableFrom(writer.getType().getRawClass())) { + beanProperties.set(i, new GuavaOptionalBeanPropertyWriter(writer)); + } + } + return beanProperties; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalBeanPropertyWriter.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalBeanPropertyWriter.java new file mode 100644 index 00000000..98d3d61d --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalBeanPropertyWriter.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.util.NameTransformer; +import com.google.common.base.Optional; + +public class GuavaOptionalBeanPropertyWriter extends BeanPropertyWriter +{ + private static final long serialVersionUID = 1; + + protected GuavaOptionalBeanPropertyWriter(BeanPropertyWriter base) { + super(base); + } + + protected GuavaOptionalBeanPropertyWriter(BeanPropertyWriter base, PropertyName newName) { + super(base, newName); + } + + @Override + protected BeanPropertyWriter _new(PropertyName newName) { + return new GuavaOptionalBeanPropertyWriter(this, newName); + } + + @Override + public BeanPropertyWriter unwrappingWriter(NameTransformer unwrapper) { + return new GuavaUnwrappingOptionalBeanPropertyWriter(this, unwrapper); + } + + @Override + public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception + { + if (_nullSerializer == null) { + Object value = get(bean); + if (value == null || Optional.absent().equals(value)) { + return; + } + } + super.serializeAsField(bean, gen, prov); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalSerializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalSerializer.java new file mode 100644 index 00000000..be24d788 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaOptionalSerializer.java @@ -0,0 +1,347 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import java.io.IOException; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.ReferenceType; +import com.fasterxml.jackson.databind.util.NameTransformer; +import com.google.common.base.Optional; + +@SuppressWarnings("serial") +public final class GuavaOptionalSerializer + extends StdSerializer> + implements ContextualSerializer +{ + /** + * Declared type parameter for Optional. + */ + protected final JavaType _referredType; + + protected final BeanProperty _property; + + protected final TypeSerializer _valueTypeSerializer; + protected final JsonSerializer _valueSerializer; + + /** + * Further guidance on serialization-inclusion (or not), regarding + * contained value (if any). + * + * @since 2.7 + */ + protected final JsonInclude.Include _contentInclusion; + + /** + * To support unwrapped values of dynamic types, will need this: + */ + protected final NameTransformer _unwrapper; + + /** + * If element type can not be statically determined, mapping from + * runtime type to serializer is handled using this object + * + * @since 2.6 + */ + protected transient PropertySerializerMap _dynamicSerializers; + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + @SuppressWarnings("unchecked") + public GuavaOptionalSerializer(ReferenceType fullType, + TypeSerializer vts, JsonSerializer valueSer) + { + super(fullType); + _referredType = fullType.getReferencedType(); + _property = null; + _valueTypeSerializer = vts; + _valueSerializer = (JsonSerializer) valueSer; + _unwrapper = null; + _contentInclusion = null; + _dynamicSerializers = PropertySerializerMap.emptyForProperties(); + } + + @SuppressWarnings("unchecked") + protected GuavaOptionalSerializer(GuavaOptionalSerializer base, + BeanProperty property, + TypeSerializer vts, JsonSerializer valueSer, + NameTransformer unwrapper, JsonInclude.Include contentIncl) + { + super(base); + _referredType = base._referredType; + _dynamicSerializers = base._dynamicSerializers; + _property = property; + _valueTypeSerializer = vts; + _valueSerializer = (JsonSerializer) valueSer; + _unwrapper = unwrapper; + if ((contentIncl == JsonInclude.Include.USE_DEFAULTS) + || (contentIncl == JsonInclude.Include.ALWAYS)) { + _contentInclusion = null; + } else { + _contentInclusion = contentIncl; + } + } + + @Override + public JsonSerializer> unwrappingSerializer(NameTransformer transformer) { + JsonSerializer ser = _valueSerializer; + if (ser != null) { + ser = ser.unwrappingSerializer(transformer); + } + NameTransformer unwrapper = (_unwrapper == null) ? transformer + : NameTransformer.chainedTransformer(transformer, _unwrapper); + return withResolved(_property, _valueTypeSerializer, ser, unwrapper, _contentInclusion); + } + + protected GuavaOptionalSerializer withResolved(BeanProperty prop, + TypeSerializer vts, JsonSerializer valueSer, + NameTransformer unwrapper, JsonInclude.Include contentIncl) + { + if ((_property == prop) && (contentIncl == _contentInclusion) + && (_valueTypeSerializer == vts) && (_valueSerializer == valueSer) + && (_unwrapper == unwrapper)) { + return this; + } + return new GuavaOptionalSerializer(this, prop, vts, valueSer, unwrapper, contentIncl); + } + + /* + /********************************************************** + /* Contextualization (support for property annotations) + /********************************************************** + */ + + @Override + public JsonSerializer createContextual(SerializerProvider provider, + BeanProperty property) throws JsonMappingException + { + TypeSerializer vts = _valueTypeSerializer; + if (vts != null) { + vts = vts.forProperty(property); + } + JsonSerializer ser = findContentSerializer(provider, property); + if (ser == null) { + ser = _valueSerializer; + if (ser == null) { + // A few conditions needed to be able to fetch serializer here: + if (_useStatic(provider, property, _referredType)) { + ser = _findSerializer(provider, _referredType, property); + } + } else { + ser = provider.handlePrimaryContextualization(ser, property); + } + } + // Also: may want to have more refined exclusion based on referenced value + JsonInclude.Include contentIncl = _contentInclusion; + if (property != null) { + JsonInclude.Value incl = property.findPropertyInclusion(provider.getConfig(), + Optional.class); + JsonInclude.Include newIncl = incl.getContentInclusion(); + if ((newIncl != contentIncl) && (newIncl != JsonInclude.Include.USE_DEFAULTS)) { + contentIncl = newIncl; + } + } + return withResolved(property, vts, ser, _unwrapper, contentIncl); + } + + protected boolean _useStatic(SerializerProvider provider, BeanProperty property, + JavaType referredType) + { + // First: no serializer for `Object.class`, must be dynamic + if (referredType.isJavaLangObject()) { + return false; + } + // but if type is final, might as well fetch + if (referredType.isFinal()) { // or should we allow annotation override? (only if requested...) + return true; + } + // also: if indicated by typing, should be considered static + if (referredType.useStaticType()) { + return true; + } + // if neither, maybe explicit annotation? + AnnotationIntrospector intr = provider.getAnnotationIntrospector(); + if ((intr != null) && (property != null)) { + Annotated ann = property.getMember(); + if (ann != null) { + JsonSerialize.Typing t = intr.findSerializationTyping(property.getMember()); + if (t == JsonSerialize.Typing.STATIC) { + return true; + } + if (t == JsonSerialize.Typing.DYNAMIC) { + return false; + } + } + } + // and finally, may be forced by global static typing (unlikely...) + return provider.isEnabled(MapperFeature.USE_STATIC_TYPING); + } + + /* + /********************************************************** + /* API overrides + /********************************************************** + */ + + @Override + public boolean isEmpty(SerializerProvider provider, Optional value) + { + if ((value == null) || !value.isPresent()) { + return true; + } + if (_contentInclusion == null) { + return false; + } + Object contents = value.get(); + JsonSerializer ser = _valueSerializer; + if (ser == null) { + try { + ser = _findCachedSerializer(provider, value.getClass()); + } catch (JsonMappingException e) { // nasty but necessary + throw new RuntimeJsonMappingException(e); + } + } + return ser.isEmpty(provider, contents); + } + + @Override + public boolean isUnwrappingSerializer() { + return (_unwrapper != null); + } + + /* + /********************************************************** + /* Serialization methods + /********************************************************** + */ + + @Override + public void serialize(Optional opt, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (!opt.isPresent()) { + // 22-Oct-2015, tatu: With unwrapping we can not serialize value, just key/value pairs so: + if (_unwrapper == null) { + provider.defaultSerializeNull(gen); + } + return; + } + Object value = opt.get(); + JsonSerializer ser = _valueSerializer; + if (ser == null) { + ser = _findCachedSerializer(provider, value.getClass()); + } + if (_valueTypeSerializer != null) { + ser.serializeWithType(value, gen, provider, _valueTypeSerializer); + } else { + ser.serialize(value, gen, provider); + } + } + + @Override + public void serializeWithType(Optional opt, + JsonGenerator gen, SerializerProvider provider, + TypeSerializer typeSer) throws IOException + { + if (!opt.isPresent()) { + if (_unwrapper == null) { + provider.defaultSerializeNull(gen); + } + return; + } + // Otherwise apply type-prefix/suffix, then std serialize: + typeSer.writeTypePrefixForScalar(opt, gen, Optional.class); + serialize(opt, gen, provider); + typeSer.writeTypeSuffixForScalar(opt, gen); + } + + /* + /********************************************************** + /* Introspection support + /********************************************************** + */ + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException + { + JsonSerializer ser = _valueSerializer; + if (ser == null) { + // 28-Sep-2015, tatu: as per [datatype-guava#83] need to ensure we don't + // accidentally drop parameterization + ser = _findSerializer(visitor.getProvider(), _referredType, _property); + if (_unwrapper != null) { + ser = ser.unwrappingSerializer(_unwrapper); + } + } + ser.acceptJsonFormatVisitor(visitor, _referredType); + } + + /* + /********************************************************** + /* Misc other + /********************************************************** + */ + + /** + * Helper method that encapsulates logic of retrieving and caching required + * serializer. + */ + protected final JsonSerializer _findCachedSerializer(SerializerProvider provider, + Class type) throws JsonMappingException + { + JsonSerializer ser = _dynamicSerializers.serializerFor(type); + if (ser == null) { + ser = _findSerializer(provider, type, _property); + if (_unwrapper != null) { + ser = ser.unwrappingSerializer(_unwrapper); + } + _dynamicSerializers = _dynamicSerializers.newWith(type, ser); + } + return ser; + } + + private final JsonSerializer _findSerializer(SerializerProvider provider, + Class type, BeanProperty prop) throws JsonMappingException + { + // Important: ask for TYPED serializer, in case polymorphic handling is needed! + return provider.findTypedValueSerializer(type, true, prop); + } + + private final JsonSerializer _findSerializer(SerializerProvider provider, + JavaType type, BeanProperty prop) throws JsonMappingException + { + // Important: ask for TYPED serializer, in case polymorphic handling is needed! + return provider.findTypedValueSerializer(type, true, prop); + } + + // !!! TODO: added late in 2.7 in `jackson-databind`: remove from 2.8 + protected JsonSerializer findContentSerializer(SerializerProvider serializers, + BeanProperty property) + throws JsonMappingException + { + if (property != null) { + AnnotatedMember m = property.getMember(); + final AnnotationIntrospector intr = serializers.getAnnotationIntrospector(); + if (m != null) { + Object serDef = intr.findContentSerializer(m); + if (serDef != null) { + return serializers.serializerInstance(m, serDef); + } + } + } + return null; + } + +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaUnwrappingOptionalBeanPropertyWriter.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaUnwrappingOptionalBeanPropertyWriter.java new file mode 100644 index 00000000..5c1e3a30 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/GuavaUnwrappingOptionalBeanPropertyWriter.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.io.SerializedString; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter; +import com.fasterxml.jackson.databind.util.NameTransformer; +import com.google.common.base.Optional; + +public class GuavaUnwrappingOptionalBeanPropertyWriter extends UnwrappingBeanPropertyWriter +{ + private static final long serialVersionUID = 1L; + + public GuavaUnwrappingOptionalBeanPropertyWriter(BeanPropertyWriter base, + NameTransformer transformer) { + super(base, transformer); + } + + protected GuavaUnwrappingOptionalBeanPropertyWriter(UnwrappingBeanPropertyWriter base, + NameTransformer transformer, SerializedString name) { + super(base, transformer, name); + } + + @Override + protected UnwrappingBeanPropertyWriter _new(NameTransformer transformer, SerializedString newName) + { + return new GuavaUnwrappingOptionalBeanPropertyWriter(this, transformer, newName); + } + + @Override + public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception + { + if (_nullSerializer == null) { + Object value = get(bean); + if (value == null || Optional.absent().equals(value)) { + return; + } + } + super.serializeAsField(bean, gen, prov); + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/MultimapSerializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/MultimapSerializer.java new file mode 100644 index 00000000..e2e6ed97 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/MultimapSerializer.java @@ -0,0 +1,409 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import java.io.IOException; +import java.util.*; +import java.util.Map.Entry; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContainerSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.PropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; +import com.fasterxml.jackson.databind.ser.std.MapProperty; +import com.fasterxml.jackson.databind.type.MapLikeType; + +import com.google.common.collect.Multimap; + +/** + * Serializer for Guava's {@link Multimap} values. Output format encloses all + * value sets in JSON Array, regardless of number of values; this to reduce + * complexity (and inaccuracy) of trying to handle cases where values themselves + * would be serialized as arrays (in which cases determining whether given array + * is a wrapper or value gets complicated and unreliable). + *

+ * Missing features, compared to standard Java Maps: + *

    + *
  • Inclusion checks for content entries (non-null, non-empty) + *
  • + *
  • Sorting of entries + *
  • + *
+ */ +public class MultimapSerializer + extends ContainerSerializer> + implements ContextualSerializer +{ + private static final long serialVersionUID = 1L; + + private final MapLikeType _type; + private final BeanProperty _property; + private final JsonSerializer _keySerializer; + private final TypeSerializer _valueTypeSerializer; + private final JsonSerializer _valueSerializer; + + /** + * Set of entries to omit during serialization, if any + * + * @since 2.5 + */ + protected final Set _ignoredEntries; + + /** + * If value type can not be statically determined, mapping from + * runtime value types to serializers are stored in this object. + * + * @since 2.5 + */ + protected PropertySerializerMap _dynamicValueSerializers; + + /** + * Id of the property filter to use, if any; null if none. + * + * @since 2.5 + */ + protected final Object _filterId; + + /** + * Flag set if output is forced to be sorted by keys (usually due + * to annotation). + *

+ * NOTE: not yet used. + * + * @since 2.5 + */ + protected final boolean _sortKeys; + + public MultimapSerializer(MapLikeType type, BeanDescription beanDesc, + JsonSerializer keySerializer, TypeSerializer vts, JsonSerializer valueSerializer, + Set ignoredEntries, Object filterId) + { + super(type.getRawClass(), false); + _type = type; + _property = null; + _keySerializer = keySerializer; + _valueTypeSerializer = vts; + _valueSerializer = valueSerializer; + _ignoredEntries = ignoredEntries; + _filterId = filterId; + _sortKeys = false; + + _dynamicValueSerializers = PropertySerializerMap.emptyForProperties(); + } + + /** + * @since 2.5 + */ + @SuppressWarnings("unchecked") + protected MultimapSerializer(MultimapSerializer src, BeanProperty property, + JsonSerializer keySerializer, TypeSerializer vts, JsonSerializer valueSerializer, + Set ignoredEntries, Object filterId, boolean sortKeys) + { + super(src); + _type = src._type; + _property = property; + _keySerializer = (JsonSerializer) keySerializer; + _valueTypeSerializer = vts; + _valueSerializer = (JsonSerializer) valueSerializer; + _dynamicValueSerializers = src._dynamicValueSerializers; + _ignoredEntries = ignoredEntries; + _filterId = filterId; + _sortKeys = sortKeys; + } + + protected MultimapSerializer withResolved(BeanProperty property, + JsonSerializer keySer, TypeSerializer vts, JsonSerializer valueSer, + Set ignored, Object filterId, boolean sortKeys) + { + return new MultimapSerializer(this, property, keySer, vts, valueSer, + ignored, filterId, sortKeys); + } + + @Override + protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSer) { + return new MultimapSerializer(this, _property, _keySerializer, + typeSer, _valueSerializer, _ignoredEntries, _filterId, _sortKeys); + } + + /* + /********************************************************** + /* Post-processing (contextualization) + /********************************************************** + */ + + @Override + public JsonSerializer createContextual(SerializerProvider provider, + BeanProperty property) throws JsonMappingException + { + JsonSerializer valueSer = _valueSerializer; + if (valueSer == null) { // if type is final, can actually resolve: + JavaType valueType = _type.getContentType(); + if (valueType.isFinal()) { + valueSer = provider.findValueSerializer(valueType, property); + } + } else if (valueSer instanceof ContextualSerializer) { + valueSer = ((ContextualSerializer) valueSer).createContextual(provider, property); + } + + final AnnotationIntrospector intr = provider.getAnnotationIntrospector(); + final AnnotatedMember propertyAcc = (property == null) ? null : property.getMember(); + JsonSerializer keySer = null; + Object filterId = _filterId; + + // First: if we have a property, may have property-annotation overrides + if (propertyAcc != null && intr != null) { + Object serDef = intr.findKeySerializer(propertyAcc); + if (serDef != null) { + keySer = provider.serializerInstance(propertyAcc, serDef); + } + serDef = intr.findContentSerializer(propertyAcc); + if (serDef != null) { + valueSer = provider.serializerInstance(propertyAcc, serDef); + } + filterId = intr.findFilterId(propertyAcc); + } + if (valueSer == null) { + valueSer = _valueSerializer; + } + // [datatype-guava#124]: May have a content converter + valueSer = findConvertingContentSerializer(provider, property, valueSer); + if (valueSer == null) { + // One more thing -- if explicit content type is annotated, + // we can consider it a static case as well. + JavaType valueType = _type.getContentType(); + if (valueType.useStaticType()) { + valueSer = provider.findValueSerializer(valueType, property); + } + } else { + valueSer = provider.handleSecondaryContextualization(valueSer, property); + } + if (keySer == null) { + keySer = _keySerializer; + } + if (keySer == null) { + keySer = provider.findKeySerializer(_type.getKeyType(), property); + } else { + keySer = provider.handleSecondaryContextualization(keySer, property); + } + // finally, TypeSerializers may need contextualization as well + TypeSerializer typeSer = _valueTypeSerializer; + if (typeSer != null) { + typeSer = typeSer.forProperty(property); + } + + Set ignored = _ignoredEntries; + boolean sortKeys = false; + if (intr != null && propertyAcc != null) { + String[] moreToIgnore = intr.findPropertiesToIgnore(propertyAcc, true); + if (moreToIgnore != null) { + ignored = (ignored == null) ? new HashSet() : new HashSet(ignored); + for (String str : moreToIgnore) { + ignored.add(str); + } + } + Boolean b = intr.findSerializationSortAlphabetically(propertyAcc); + sortKeys = (b != null) && b.booleanValue(); + } + return withResolved(property, keySer, typeSer, valueSer, + ignored, filterId, sortKeys); + } + + /* + /********************************************************** + /* Accessors for ContainerSerializer + /********************************************************** + */ + + @Override + public JsonSerializer getContentSerializer() { + return _valueSerializer; + } + + @Override + public JavaType getContentType() { + return _type.getContentType(); + } + + @Override + public boolean hasSingleElement(Multimap map) { + return map.size() == 1; + } + + @Override + @Deprecated // since 2.5 + public boolean isEmpty(Multimap value) { + return isEmpty(null, value); + } + + @Override + public boolean isEmpty(SerializerProvider prov, Multimap value) { + return value.isEmpty(); + } + + /* + /********************************************************** + /* Post-processing (contextualization) + /********************************************************** + */ + + @Override + public void serialize(Multimap value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + gen.writeStartObject(); + // [databind#631]: Assign current value, to be accessible by custom serializers + gen.setCurrentValue(value); + if (!value.isEmpty()) { + if (_filterId != null) { + serializeOptionalFields(value, gen, provider); + } else { + serializeFields(value, gen, provider); + } + } + gen.writeEndObject(); + } + + @Override + public void serializeWithType(Multimap value, JsonGenerator gen, SerializerProvider provider, + TypeSerializer typeSer) + throws IOException + { + typeSer.writeTypePrefixForObject(value, gen); + gen.setCurrentValue(value); + if (!value.isEmpty()) { + if (_filterId != null) { + serializeOptionalFields(value, gen, provider); + } else { + serializeFields(value, gen, provider); + } + } + typeSer.writeTypeSuffixForObject(value, gen); + } + + private final void serializeFields(Multimap mmap, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + final Set ignored = _ignoredEntries; + PropertySerializerMap serializers = _dynamicValueSerializers; + for (Entry> entry : mmap.asMap().entrySet()) { + // First, serialize key + Object key = entry.getKey(); + if ((ignored != null) && ignored.contains(key)) { + continue; + } + if (key == null) { + provider.findNullKeySerializer(_type.getKeyType(), _property) + .serialize(null, gen, provider); + } else { + _keySerializer.serialize(key, gen, provider); + } + // note: value is a List, but generic type is for contents... so: + gen.writeStartArray(); + for (Object vv : entry.getValue()) { + if (vv == null) { + provider.defaultSerializeNull(gen); + continue; + } + JsonSerializer valueSer = _valueSerializer; + if (valueSer == null) { + Class cc = vv.getClass(); + valueSer = serializers.serializerFor(cc); + if (valueSer == null) { + valueSer = _findAndAddDynamic(serializers, cc, provider); + serializers = _dynamicValueSerializers; + } + } + if (_valueTypeSerializer == null) { + valueSer.serialize(vv, gen, provider); + } else { + valueSer.serializeWithType(vv, gen, provider, _valueTypeSerializer); + } + } + gen.writeEndArray(); + } + } + + private final void serializeOptionalFields(Multimap mmap, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + final Set ignored = _ignoredEntries; + PropertyFilter filter = findPropertyFilter(provider, _filterId, mmap); + final MapProperty prop = new MapProperty(_valueTypeSerializer, _property); + for (Entry> entry : mmap.asMap().entrySet()) { + // First, serialize key + Object key = entry.getKey(); + if ((ignored != null) && ignored.contains(key)) { + continue; + } + Collection value = entry.getValue(); + JsonSerializer valueSer; + if (value == null) { + // !!! TODO: null suppression? + valueSer = provider.getDefaultNullValueSerializer(); + } else { + valueSer = _valueSerializer; + } + prop.reset(key, _keySerializer, valueSer); + try { + filter.serializeAsField(value, gen, provider, prop); + } catch (Exception e) { + String keyDesc = ""+key; + wrapAndThrow(provider, e, value, keyDesc); + } + } + } + + /* + /********************************************************** + /* Schema related functionality + /********************************************************** + */ + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + throws JsonMappingException + { + JsonMapFormatVisitor v2 = (visitor == null) ? null : visitor.expectMapFormat(typeHint); + if (v2 != null) { + v2.keyFormat(_keySerializer, _type.getKeyType()); + JsonSerializer valueSer = _valueSerializer; + JavaType vt = _type.getContentType(); + if (valueSer == null) { + valueSer = _findAndAddDynamic(_dynamicValueSerializers, + vt, visitor.getProvider()); + } + v2.valueFormat(valueSer, vt); + } + } + + /* + /********************************************************** + /* Internal helper methods + /********************************************************** + */ + + protected final JsonSerializer _findAndAddDynamic(PropertySerializerMap map, + Class type, SerializerProvider provider) throws JsonMappingException + { + PropertySerializerMap.SerializerAndMapResult result = map.findAndAddSecondarySerializer(type, provider, _property); + // did we get a new map of serializers? If so, start using it + if (map != result.map) { + _dynamicValueSerializers = result.map; + } + return result.serializer; + } + + protected final JsonSerializer _findAndAddDynamic(PropertySerializerMap map, + JavaType type, SerializerProvider provider) throws JsonMappingException + { + PropertySerializerMap.SerializerAndMapResult result = map.findAndAddSecondarySerializer(type, provider, _property); + if (map != result.map) { + _dynamicValueSerializers = result.map; + } + return result.serializer; + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/RangeSerializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/RangeSerializer.java new file mode 100644 index 00000000..cf653312 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/RangeSerializer.java @@ -0,0 +1,149 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +/** + * Jackson serializer for a Guava {@link Range}. + */ +@SuppressWarnings("serial") +public class RangeSerializer extends StdSerializer> + implements ContextualSerializer +{ + protected final JavaType _rangeType; + + protected final JsonSerializer _endpointSerializer; + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + public RangeSerializer(JavaType type) { this(type, null); } + + @SuppressWarnings("unchecked") + public RangeSerializer(JavaType type, JsonSerializer endpointSer) + { + super(type); + _rangeType = type; + _endpointSerializer = (JsonSerializer) endpointSer; + } + + @Override + @Deprecated + public boolean isEmpty(Range value) { + return isEmpty(null, value); + } + + @Override + public boolean isEmpty(SerializerProvider prov, Range value) { + return value.isEmpty(); + } + + @Override + public JsonSerializer createContextual(SerializerProvider prov, + BeanProperty property) throws JsonMappingException + { + if (_endpointSerializer == null) { + JavaType endpointType = _rangeType.containedTypeOrUnknown(0); + // let's not consider "untyped" (java.lang.Object) to be meaningful here... + if (endpointType != null && !endpointType.hasRawClass(Object.class)) { + JsonSerializer ser = prov.findValueSerializer(endpointType, property); + return new RangeSerializer(_rangeType, ser); + } + /* 21-Sep-2014, tatu: Need to make sure all serializers get proper contextual + * access, in case they rely on annotations on properties... (or, more generally, + * in getting a chance to be contextualized) + */ + } else if (_endpointSerializer instanceof ContextualSerializer) { + JsonSerializer cs = ((ContextualSerializer)_endpointSerializer).createContextual(prov, property); + if (cs != _endpointSerializer) { + return new RangeSerializer(_rangeType, cs); + } + } + return this; + } + + /* + /********************************************************** + /* Serialization methods + /********************************************************** + */ + + @Override + public void serialize(Range value, JsonGenerator gen, SerializerProvider provider) + throws IOException, JsonGenerationException + { + gen.writeStartObject(); + _writeContents(value, gen, provider); + gen.writeEndObject(); + + } + + @Override + public void serializeWithType(Range value, JsonGenerator gen, SerializerProvider provider, + TypeSerializer typeSer) + throws IOException, JsonProcessingException + { + // Will be serialized as a JSON Object, so: + typeSer.writeTypePrefixForObject(value, gen); + _writeContents(value, gen, provider); + typeSer.writeTypeSuffixForObject(value, gen); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + throws JsonMappingException + { + if (visitor != null) { + JsonObjectFormatVisitor objectVisitor = visitor.expectObjectFormat(typeHint); + if (objectVisitor != null) { + if (_endpointSerializer != null) { + JavaType endpointType = _rangeType.containedType(0); + JavaType btType = visitor.getProvider().constructType(BoundType.class); + JsonSerializer btSer = visitor.getProvider() + .findValueSerializer(btType, null); + objectVisitor.property("lowerEndpoint", _endpointSerializer, endpointType); + objectVisitor.property("lowerBoundType", btSer, btType); + objectVisitor.property("upperEndpoint", _endpointSerializer, endpointType); + objectVisitor.property("upperBoundType", btSer, btType); + } + } + } + } + + private void _writeContents(Range value, JsonGenerator jgen, SerializerProvider provider) + throws IOException + { + if (value.hasLowerBound()) { + if (_endpointSerializer != null) { + jgen.writeFieldName("lowerEndpoint"); + _endpointSerializer.serialize(value.lowerEndpoint(), jgen, provider); + } else { + provider.defaultSerializeField("lowerEndpoint", value.lowerEndpoint(), jgen); + } + provider.defaultSerializeField("lowerBoundType", value.lowerBoundType(), jgen); + } + if (value.hasUpperBound()) { + if (_endpointSerializer != null) { + jgen.writeFieldName("upperEndpoint"); + _endpointSerializer.serialize(value.upperEndpoint(), jgen, provider); + } else { + provider.defaultSerializeField("upperEndpoint", value.upperEndpoint(), jgen); + } + provider.defaultSerializeField("upperBoundType", value.upperBoundType(), jgen); + } + } +} diff --git a/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java new file mode 100644 index 00000000..13726f20 --- /dev/null +++ b/guava/src/main/java/com/fasterxml/jackson/datatype/guava/ser/TableSerializer.java @@ -0,0 +1,233 @@ +package com.fasterxml.jackson.datatype.guava.ser; + +import java.io.IOException; +import java.util.Map; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.ContainerSerializer; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.MapSerializer; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import com.google.common.collect.Table; + +/** + * @author stevenmhood (via hyandell) - Initial implementation + * @author tatu - Some refactoring to streamline code + */ +public class TableSerializer + extends ContainerSerializer> implements ContextualSerializer +{ + private static final long serialVersionUID = -1449462718192917949L; + + /** + * Type declaration that defines parameters; may be a supertype of actual + * type of property being serialized. + */ + private final JavaType _type; + + private final BeanProperty _property; + + private final JsonSerializer _rowSerializer; + private final JsonSerializer _columnSerializer; + private final TypeSerializer _valueTypeSerializer; + private final JsonSerializer _valueSerializer; + + private final MapSerializer _rowMapSerializer; + + /* + /********************************************************** + /* Serializer lifecycle + /********************************************************** + */ + + public TableSerializer(final JavaType type) + { + super(type); + _type = type; + _property = null; + _rowSerializer = null; + _columnSerializer = null; + _valueTypeSerializer = null; + _valueSerializer = null; + + _rowMapSerializer = null; + } + + @SuppressWarnings( "unchecked" ) + protected TableSerializer(final TableSerializer src, + final BeanProperty property, + final TypeFactory typeFactory, + final JsonSerializer rowKeySerializer, + final JsonSerializer columnKeySerializer, + final TypeSerializer valueTypeSerializer, + final JsonSerializer valueSerializer) + { + super(src); + _type = src._type; + _property = property; + _rowSerializer = (JsonSerializer) rowKeySerializer; + _columnSerializer = (JsonSerializer) columnKeySerializer; + _valueTypeSerializer = valueTypeSerializer; + _valueSerializer = (JsonSerializer) valueSerializer; + + final MapType columnAndValueType = typeFactory.constructMapType(Map.class, + _type.containedTypeOrUnknown(1), _type.containedTypeOrUnknown(2)); + JsonSerializer columnAndValueSerializer = + MapSerializer.construct(null, + columnAndValueType, + false, + _valueTypeSerializer, + _columnSerializer, + _valueSerializer, + null); + + final MapType rowMapType = typeFactory.constructMapType(Map.class, + _type.containedTypeOrUnknown(0), columnAndValueType); + _rowMapSerializer = + MapSerializer.construct(null, + rowMapType, + false, + null, + _rowSerializer, + (JsonSerializer) columnAndValueSerializer, + null); + } + + protected TableSerializer(final TableSerializer src, TypeSerializer typeSer) + { + super(src); + _type = src._type; + _property = src._property; + _rowSerializer = src._rowSerializer; + _columnSerializer = src._columnSerializer; + _valueTypeSerializer = typeSer; + _valueSerializer = src._valueSerializer; + + _rowMapSerializer = src._rowMapSerializer; + + } + + protected TableSerializer withResolved(final BeanProperty property, + final TypeFactory typeFactory, + final JsonSerializer rowKeySer, + final JsonSerializer columnKeySer, + final TypeSerializer vts, + final JsonSerializer valueSer ) + { + return new TableSerializer(this, property, typeFactory, + rowKeySer, columnKeySer, vts, valueSer); + } + + @Override + protected ContainerSerializer _withValueTypeSerializer(final TypeSerializer typeSer) + { + return new TableSerializer(this, typeSer); + } + + @Override + public JsonSerializer createContextual(final SerializerProvider provider, final BeanProperty property ) throws JsonMappingException + { + JsonSerializer valueSer = _valueSerializer; + if (valueSer == null) { // if type is final, can actually resolve: + final JavaType valueType = _type.containedTypeOrUnknown(2); + if (valueType.isFinal()) { + valueSer = provider.findValueSerializer(valueType, property); + } + } + else if (valueSer instanceof ContextualSerializer) { + valueSer = ((ContextualSerializer) valueSer).createContextual(provider, property); + } + JsonSerializer rowKeySer = _rowSerializer; + if (rowKeySer == null) { + rowKeySer = provider.findKeySerializer(_type.containedTypeOrUnknown(0), property); + } + else if (rowKeySer instanceof ContextualSerializer) { + rowKeySer = ((ContextualSerializer) rowKeySer).createContextual(provider, property); + } + JsonSerializer columnKeySer = _columnSerializer; + if (columnKeySer == null) { + columnKeySer = provider.findKeySerializer(_type.containedTypeOrUnknown(1), property); + } + else if (columnKeySer instanceof ContextualSerializer) { + columnKeySer = ((ContextualSerializer) columnKeySer).createContextual(provider, property); + } + // finally, TypeSerializers may need contextualization as well + TypeSerializer typeSer = _valueTypeSerializer; + if (typeSer != null) { + typeSer = typeSer.forProperty(property); + } + return withResolved(property, provider.getTypeFactory(), rowKeySer, columnKeySer, typeSer, valueSer); + } + + /* + /********************************************************** + /* Simple accessor API + /********************************************************** + */ + + @Override + public JavaType getContentType() { + return _type.getContentType(); + } + + @Override + public JsonSerializer getContentSerializer() { + return _valueSerializer; + } + + @Override + public boolean isEmpty(SerializerProvider provider, Table table) { + return table.isEmpty(); + } + + @Override + @Deprecated // since 2.6, remove from 2.8 + public boolean isEmpty(Table table) { + return table.isEmpty(); + } + + @Override + public boolean hasSingleElement(final Table table) { + return table.size() == 1; + } + + /* + /********************************************************** + /* Main serialization methods + /********************************************************** + */ + + @Override + public void serialize(final Table value, + final JsonGenerator gen, final SerializerProvider provider) + throws IOException + { + gen.writeStartObject(); + if ( !value.isEmpty()) { + serializeFields(value, gen, provider); + } + gen.writeEndObject(); + } + + @Override + public void serializeWithType(final Table value, + final JsonGenerator gen, + final SerializerProvider provider, + final TypeSerializer typeSer) throws IOException + { + typeSer.writeTypePrefixForObject(value, gen); + serializeFields(value, gen, provider); + typeSer.writeTypeSuffixForObject(value, gen); + } + + private final void serializeFields( final Table table, final JsonGenerator jgen, final SerializerProvider provider ) + throws IOException + { + _rowMapSerializer.serializeFields(table.rowMap(), jgen, provider); + } +} diff --git a/guava/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/guava/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 00000000..047e1f8b --- /dev/null +++ b/guava/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +com.fasterxml.jackson.datatype.guava.GuavaModule diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/FluentIterableTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/FluentIterableTest.java new file mode 100644 index 00000000..c6e3aa6b --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/FluentIterableTest.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Sets; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Unit tests to verify serialization of {@link FluentIterable}s. + */ +public class FluentIterableTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + public static class FluentHolder { + public final Iterable value = createFluentIterable(); + } + + static FluentIterable createFluentIterable() { + return FluentIterable.from(Sets.newHashSet(1, 2, 3)); + } + + /** + * This test is present so that we know if either Jackson's handling of FluentIterable + * or Guava's implementation of FluentIterable changes. + * @throws Exception + */ + public void testSerializationWithoutModule() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + FluentHolder holder = new FluentHolder(); + String json = mapper.writeValueAsString(holder); + assertEquals("{\"value\":{\"empty\":false}}", json); + } + + public void testSerialization() throws Exception { + String json = MAPPER.writeValueAsString(createFluentIterable()); + assertEquals("[1,2,3]", json); + } + + public void testWrappedSerialization() throws Exception { + FluentHolder holder = new FluentHolder(); + String json = MAPPER.writeValueAsString(holder); + assertEquals("{\"value\":[1,2,3]}", json); + } + +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HashCodeTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HashCodeTest.java new file mode 100644 index 00000000..c4271b54 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HashCodeTest.java @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.hash.HashCode; + +public class HashCodeTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testSerialization() throws Exception + { + HashCode input = HashCode.fromString("cafebabe12345678"); + String json = MAPPER.writeValueAsString(input); + assertEquals("\"cafebabe12345678\"", json); + } + + public void testDeserialization() throws Exception + { + // success: + HashCode result = MAPPER.readValue(quote("0123456789cAfEbAbE"), HashCode.class); + assertEquals("0123456789cafebabe", result.toString()); + + // and ... error: + try { + result = MAPPER.readValue(quote("ghijklmn0123456789"), HashCode.class); + fail("Should not deserialize from non-hex string: got "+result); + } catch (JsonProcessingException e) { + verifyException(e, "Illegal hexadecimal character"); + } + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HostAndPortTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HostAndPortTest.java new file mode 100644 index 00000000..e409d1c3 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/HostAndPortTest.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.net.HostAndPort; + +public class HostAndPortTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testSerialization() throws Exception + { + HostAndPort input = HostAndPort.fromParts("localhost", 80); + String json = MAPPER.writeValueAsString(input); + assertEquals("\"localhost:80\"", json); + } + + public void testDeserialization() throws Exception + { + // Actually, let's support both old style and new style + + // old: + HostAndPort result = MAPPER.readValue(aposToQuotes("{'hostText':'localhost','port':9090}"), + HostAndPort.class); + assertEquals("localhost", result.getHostText()); + assertEquals(9090, result.getPort()); + + // and new: + result = MAPPER.readValue(quote("localhost:7070"), HostAndPort.class); + assertEquals("localhost", result.getHostText()); + assertEquals(7070, result.getPort()); + + // and ... error (note: numbers, booleans may all be fine) + try { + result = MAPPER.readValue("[ ]", HostAndPort.class); + fail("Should not deserialize from boolean: got "+result); + } catch (JsonProcessingException e) { + verifyException(e, "Can not deserialize"); + } + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/IterablesTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/IterablesTest.java new file mode 100644 index 00000000..ddd2e77f --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/IterablesTest.java @@ -0,0 +1,50 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +public class IterablesTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + @JsonInclude(JsonInclude.Include.NON_NULL) + static class IterableWrapper { + public Iterable values; + + public IterableWrapper(Iterable v) { values = v; } + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + public void testIterablesSerialization() throws Exception + { + String json = MAPPER.writeValueAsString(Iterables.limit(Iterables.cycle(1,2,3), 3)); + assertNotNull(json); + assertEquals("[1,2,3]", json); + } + + // for [#60] + public void testIterablesWithTransform() throws Exception + { + Iterable input = Iterables.transform(ImmutableList.of("mr", "bo", "jangles"), + new Function() { + @Override + public String apply(String x) { + return new StringBuffer(x).reverse().toString(); + } + }); + String json = MAPPER.writeValueAsString(input); + assertEquals(aposToQuotes("['rm','ob','selgnaj']"), json); + + // and then as property? + json = MAPPER.writeValueAsString(new IterableWrapper(input)); + assertEquals(aposToQuotes("{'values':['rm','ob','selgnaj']}"), json); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ModuleTestBase.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ModuleTestBase.java new file mode 100644 index 00000000..a174e2f9 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ModuleTestBase.java @@ -0,0 +1,46 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.util.Arrays; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.fasterxml.jackson.datatype.guava.GuavaModule; + +public abstract class ModuleTestBase extends junit.framework.TestCase +{ + protected ModuleTestBase() { } + + protected ObjectMapper mapperWithModule() { + return mapperWithModule(false); + } + + protected ObjectMapper mapperWithModule(boolean absentsAsNulls) + { + ObjectMapper mapper = new ObjectMapper(); + GuavaModule module = new GuavaModule(); + module.configureAbsentsAsNulls(absentsAsNulls); + mapper.registerModule(module); + return mapper; + } + + protected String aposToQuotes(String json) { + return json.replace("'", "\""); + } + + public String quote(String str) { + return '"'+str+'"'; + } + + 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+"\""); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ScalarTypesTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ScalarTypesTest.java new file mode 100644 index 00000000..9835e993 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/ScalarTypesTest.java @@ -0,0 +1,25 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.net.InternetDomainName; + +public class ScalarTypesTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testInternetDomainNameSerialization() throws Exception + { + final String INPUT = "google.com"; + InternetDomainName name = InternetDomainName.from(INPUT); + assertEquals(quote(INPUT), MAPPER.writeValueAsString(name)); + } + + public void testInternetDomainNameDeserialization() throws Exception + { + final String INPUT = "google.com"; +// InternetDomainName name = MAPPER.readValue(quote(INPUT), InternetDomainName.class); + InternetDomainName name = new ObjectMapper().readValue(quote(INPUT), InternetDomainName.class); + assertNotNull(name); + assertEquals(INPUT, name.toString()); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TableSerializationTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TableSerializationTest.java new file mode 100644 index 00000000..c1c9af62 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TableSerializationTest.java @@ -0,0 +1,165 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import com.google.common.collect.ImmutableTable; + +public class TableSerializationTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(false); + { + MAPPER.registerModule(new ComplexKeyModule()); + } + + static class ComplexKeyModule extends SimpleModule + { + private static final long serialVersionUID = 1L; + + public ComplexKeyModule() + { + this.addKeySerializer(ComplexKey.class, new JsonSerializer() { + @Override + public void serialize( final ComplexKey value, final JsonGenerator jgen, final SerializerProvider provider ) + throws IOException, JsonProcessingException + { + jgen.writeFieldName(value.getKey1() + ":" + value.getKey2()); + } + }); + + this.addKeyDeserializer(ComplexKey.class, new KeyDeserializer() { + @Override + public Object deserializeKey( final String key, final DeserializationContext ctxt ) throws IOException, JsonProcessingException + { + final String[] split = key.split(":"); + return new ComplexKey(split[0], split[1]); + } + }); + } + } + + static class ComplexKey + { + private String key1; + private String key2; + + public ComplexKey( final String key1, final String key2 ) + { + super(); + this.key1 = key1; + this.key2 = key2; + } + + public String getKey1() + { + return this.key1; + } + + public void setKey1( final String key1 ) + { + this.key1 = key1; + } + + public String getKey2() + { + return this.key2; + } + + public void setKey2( final String key2 ) + { + this.key2 = key2; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.key1 == null) ? 0 : this.key1.hashCode()); + result = prime * result + ((this.key2 == null) ? 0 : this.key2.hashCode()); + return result; + } + + @Override + public boolean equals( final Object obj ) + { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if ( !(obj instanceof ComplexKey)) { + return false; + } + final ComplexKey other = (ComplexKey) obj; + if (this.key1 == null) { + if (other.key1 != null) { + return false; + } + } + else if ( !this.key1.equals(other.key1)) { + return false; + } + if (this.key2 == null) { + if (other.key2 != null) { + return false; + } + } + else if ( !this.key2.equals(other.key2)) { + return false; + } + return true; + } + + } + + public void testSimpleKeyImmutableTableSerde() throws IOException + { + final ImmutableTable.Builder builder = ImmutableTable.builder(); + builder.put(Integer.valueOf(42), "column42", "some value 42"); + builder.put(Integer.valueOf(45), "column45", "some value 45"); + final ImmutableTable simpleTable = builder.build(); + + final String simpleJson = MAPPER.writeValueAsString(simpleTable); + assertEquals("{\"42\":{\"column42\":\"some value 42\"},\"45\":{\"column45\":\"some value 45\"}}", simpleJson); + + // !!! TODO: support deser + + /* + final ImmutableTable reconstitutedTable = + this.MAPPER.readValue(simpleJson, new TypeReference>() {}); + assertEquals(simpleTable, reconstitutedTable); + */ + } + + /** + * This test illustrates one way to use objects as keys in Tables. + */ + public void testComplexKeyImmutableTableSerde() throws IOException + { + final ImmutableTable.Builder ckBuilder = ImmutableTable.builder(); + ckBuilder.put(Integer.valueOf(42), new ComplexKey("field1", "field2"), "some value 42"); + ckBuilder.put(Integer.valueOf(45), new ComplexKey("field1", "field2"), "some value 45"); + final ImmutableTable complexKeyTable = ckBuilder.build(); + + final TypeReference> tableType = new TypeReference>() + {}; + + final String ckJson = this.MAPPER.writerFor(tableType).writeValueAsString(complexKeyTable); + assertEquals("{\"42\":{\"field1:field2\":\"some value 42\"},\"45\":{\"field1:field2\":\"some value 45\"}}", ckJson); + + // !!! TODO: support deser +/* + + final ImmutableTable reconstitutedTable = this.MAPPER.readValue(ckJson, tableType); + assertEquals(complexKeyTable, reconstitutedTable); + */ + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestImmutables.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestImmutables.java new file mode 100644 index 00000000..074c858d --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestImmutables.java @@ -0,0 +1,244 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.util.Iterator; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.*; + +/** + * Unit tests for verifying that various immutable types + * (like {@link ImmutableList}, {@link ImmutableMap} and {@link ImmutableSet}) + * work as expected. + * + * @author tsaloranta + */ +public class TestImmutables extends ModuleTestBase +{ + private final ObjectMapper MAPPER = mapperWithModule(); + + static class Holder { + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + public Object value; + + public Holder() { } + public Holder(Object v) { + value = v; + } + } + + /* + /********************************************************************** + /* Unit tests for verifying handling in absence of module registration + /********************************************************************** + */ + + /** + * Immutable types can actually be serialized as regular collections, without + * problems. + */ + public void testWithoutSerializers() throws Exception + { + ImmutableList list = ImmutableList.builder() + .add(1).add(2).add(3).build(); + assertEquals("[1,2,3]", MAPPER.writeValueAsString(list)); + + ImmutableSet set = ImmutableSet.builder() + .add("abc").add("def").build(); + assertEquals("[\"abc\",\"def\"]", MAPPER.writeValueAsString(set)); + + ImmutableMap map = ImmutableMap.builder() + .put("a", 1).put("b", 2).build(); + assertEquals("{\"a\":1,\"b\":2}", MAPPER.writeValueAsString(map)); + } + + /** + * Deserialization will fail, however. + */ + public void testWithoutDeserializers() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + try { + mapper.readValue("[1,2,3]", + new TypeReference>() { }); + fail("Expected failure for missing deserializer"); + } catch (JsonMappingException e) { + verifyException(e, "can not find a deserializer"); + } + + try { + mapper.readValue("[1,2,3]", new TypeReference>() { }); + fail("Expected failure for missing deserializer"); + } catch (JsonMappingException e) { + verifyException(e, "can not find a deserializer"); + } + + try { + mapper.readValue("[1,2,3]", new TypeReference>() { }); + fail("Expected failure for missing deserializer"); + } catch (JsonMappingException e) { + verifyException(e, "can not find a deserializer"); + } + + try { + mapper.readValue("{\"a\":true,\"b\":false}", new TypeReference>() { }); + fail("Expected failure for missing deserializer"); + } catch (JsonMappingException e) { + verifyException(e, "can not find a deserializer"); + } + } + + /* + /********************************************************************** + /* Unit tests for actual registered module + /********************************************************************** + */ + + public void testImmutableList() throws Exception + { + ImmutableList list = MAPPER.readValue("[1,2,3]", new TypeReference>() { }); + assertEquals(3, list.size()); + assertEquals(Integer.valueOf(1), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + assertEquals(Integer.valueOf(3), list.get(2)); + } + + public void testImmutableSet() throws Exception + { + ImmutableSet set = MAPPER.readValue("[3,7,8]", + new TypeReference>() { }); + assertEquals(3, set.size()); + Iterator it = set.iterator(); + assertEquals(Integer.valueOf(3), it.next()); + assertEquals(Integer.valueOf(7), it.next()); + assertEquals(Integer.valueOf(8), it.next()); + + set = MAPPER.readValue("[ ]", + new TypeReference>() { }); + assertEquals(0, set.size()); + } + + public void testImmutableSetFromSingle() throws Exception + { + ObjectMapper mapper = mapperWithModule() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + ImmutableSet set = mapper.readValue("\"abc\"", + new TypeReference>() { }); + assertEquals(1, set.size()); + assertTrue(set.contains("abc")); + } + + public void testTypedImmutableset() throws Exception + { + ImmutableSet set; + Holder h; + String json; + Holder result; + + // First, with one entry + set = new ImmutableSet.Builder() + .add(1).build(); + h = new Holder(set); + json = MAPPER.writeValueAsString(h); + + // so far so good. and back? + result = MAPPER.readValue(json, Holder.class); + assertNotNull(result.value); + if (!(result.value instanceof ImmutableSet)) { + fail("Expected ImmutableSet, got "+result.value.getClass()); + } + assertEquals(1, ((ImmutableSet) result.value).size()); + // and then an empty version: + set = new ImmutableSet.Builder().build(); + h = new Holder(set); + json = MAPPER.writeValueAsString(h); + result = MAPPER.readValue(json, Holder.class); + assertNotNull(result.value); + if (!(result.value instanceof ImmutableSet)) { + fail("Expected ImmutableSet, got "+result.value.getClass()); + } + assertEquals(0, ((ImmutableSet) result.value).size()); + } + + public void testImmutableSortedSet() throws Exception + { + ImmutableSortedSet set = MAPPER.readValue("[5,1,2]", new TypeReference>() { }); + assertEquals(3, set.size()); + Iterator it = set.iterator(); + assertEquals(Integer.valueOf(1), it.next()); + assertEquals(Integer.valueOf(2), it.next()); + assertEquals(Integer.valueOf(5), it.next()); + } + + public void testImmutableMap() throws Exception + { + final JavaType type = MAPPER.getTypeFactory().constructType(new TypeReference>() { }); + ImmutableMap map = MAPPER.readValue("{\"12\":true,\"4\":false}", type); + assertEquals(2, map.size()); + assertEquals(Boolean.TRUE, map.get(Integer.valueOf(12))); + assertEquals(Boolean.FALSE, map.get(Integer.valueOf(4))); + + map = MAPPER.readValue("{}", type); + assertNotNull(map); + assertEquals(0, map.size()); + + // and for [datatype-guava#52], verify allowance of JSON nulls + map = MAPPER.readValue("{\"12\":true,\"4\":null}", type); + assertEquals(1, map.size()); + } + + public void testTypedImmutableMap() throws Exception + { + ImmutableMap map; + Holder h; + String json; + Holder result; + + // First, with one entry + map = new ImmutableMap.Builder() + .put("a", 1).build(); + h = new Holder(map); + json = MAPPER.writeValueAsString(h); + + // so far so good. and back? + result = MAPPER.readValue(json, Holder.class); + assertNotNull(result.value); + if (!(result.value instanceof ImmutableMap)) { + fail("Expected ImmutableMap, got "+result.value.getClass()); + } + assertEquals(1, ((ImmutableMap) result.value).size()); + // and then an empty version: + map = new ImmutableMap.Builder().build(); + h = new Holder(map); + json = MAPPER.writeValueAsString(h); + result = MAPPER.readValue(json, Holder.class); + assertNotNull(result.value); + if (!(result.value instanceof ImmutableMap)) { + fail("Expected ImmutableMap, got "+result.value.getClass()); + } + assertEquals(0, ((ImmutableMap) result.value).size()); + } + + public void testImmutableSortedMap() throws Exception + { + ImmutableSortedMap map = MAPPER.readValue("{\"12\":true,\"4\":false}", new TypeReference>() { }); + assertEquals(2, map.size()); + assertEquals(Boolean.TRUE, map.get(Integer.valueOf(12))); + assertEquals(Boolean.FALSE, map.get(Integer.valueOf(4))); + } + + public void testImmutableBiMap() throws Exception + { + ImmutableBiMap map = MAPPER.readValue("{\"12\":true,\"4\":false}", new TypeReference>() { }); + assertEquals(2, map.size()); + assertEquals(Boolean.TRUE, map.get(12)); + assertEquals(Boolean.FALSE, map.get(4)); + assertEquals(map.get(12), Boolean.TRUE); + assertEquals(map.get(4), Boolean.FALSE); + } + +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultimaps.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultimaps.java new file mode 100644 index 00000000..d0cc27a7 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultimaps.java @@ -0,0 +1,312 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.guava.pojo.AddOp; +import com.fasterxml.jackson.datatype.guava.pojo.MathOp; +import com.fasterxml.jackson.datatype.guava.pojo.MulOp; + +import com.google.common.collect.*; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +import static com.google.common.collect.TreeMultimap.create; + +/** + * Unit tests to verify handling of various {@link Multimap}s. + * + * @author steven@nesscomputing.com + */ +public class TestMultimaps extends ModuleTestBase +{ + // Test for issue #13 on github, provided by stevenschlansker + public static enum MyEnum { + YAY, + BOO + } + + // [Issue#41] + @JsonInclude(JsonInclude.Include.NON_EMPTY) + static class MultiMapWrapper { + @JsonProperty + Multimap map = ArrayListMultimap.create(); + } + + static class MultiMapWithIgnores { + @JsonIgnoreProperties({ "x", "y" }) + public Multimap map = ArrayListMultimap.create(); + + public MultiMapWithIgnores() + { + map.put("a", "foo"); + map.put("x", "bar"); + } + } + + public static class ImmutableMultimapWrapper { + + private ImmutableMultimap multimap; + + public ImmutableMultimapWrapper() { + } + + public ImmutableMultimapWrapper(ImmutableMultimap f) { + this.multimap = f; + } + + public ImmutableMultimap getMultimap() { + return multimap; + } + + public void setMultimap(ImmutableMultimap multimap) { + this.multimap = multimap; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 97 * hash + (this.multimap != null ? this.multimap.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ImmutableMultimapWrapper other = (ImmutableMultimapWrapper) obj; + return !(this.multimap != other.multimap && (this.multimap == null || !this.multimap.equals(other.multimap))); + } + + } + + private static final String STRING_STRING_MULTIMAP = + "{\"first\":[\"abc\",\"abc\",\"foo\"]," + "\"second\":[\"bar\"]}"; + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testMultimap() throws Exception + { + _testMultimap(TreeMultimap.create(), true, + "{\"false\":[false],\"maybe\":[false,true],\"true\":[true]}"); + _testMultimap(LinkedListMultimap.create(), false, + "{\"true\":[true],\"false\":[false],\"maybe\":[true,false]}"); + _testMultimap(LinkedHashMultimap.create(), false, null); + } + + private void _testMultimap(Multimap map0, boolean fullyOrdered, String EXPECTED) throws Exception + { + @SuppressWarnings("unchecked") + Multimap map = (Multimap) map0; + map.put("true", Boolean.TRUE); + map.put("false", Boolean.FALSE); + map.put("maybe", Boolean.TRUE); + map.put("maybe", Boolean.FALSE); + + // Test that typed writes work + if (EXPECTED != null) { + String json = MAPPER.writerFor(new TypeReference>() {}).writeValueAsString(map); + assertEquals(EXPECTED, json); + } + + // And untyped too + String serializedForm = MAPPER.writeValueAsString(map); + + if (EXPECTED != null) { + assertEquals(EXPECTED, serializedForm); + } + + // these seem to be order-sensitive as well, so only use for ordered-maps + if (fullyOrdered) { + assertEquals(map, MAPPER.>readValue(serializedForm, new TypeReference>() {})); + assertEquals(map, create(MAPPER.>readValue(serializedForm, new TypeReference>() {}))); + assertEquals(map, create(MAPPER.>readValue(serializedForm, new TypeReference>() {}))); + assertEquals(map, create(MAPPER.>readValue(serializedForm, new TypeReference>() {}))); + } + } + + public void testMultimapIssue3() throws Exception + { + Multimap m1 = TreeMultimap.create(); + m1.put("foo", "bar"); + m1.put("foo", "baz"); + m1.put("qux", "quux"); + ObjectMapper o = MAPPER; + + String t1 = o.writerFor(new TypeReference>(){}).writeValueAsString(m1); + Map javaMap = o.readValue(t1, Map.class); + assertEquals(2, javaMap.size()); + + String t2 = o.writerFor(new TypeReference>(){}).writeValueAsString(m1); + javaMap = o.readValue(t2, Map.class); + assertEquals(2, javaMap.size()); + + TreeMultimap m2 = TreeMultimap.create(); + m2.put("foo", "bar"); + m2.put("foo", "baz"); + m2.put("qux", "quux"); + + String t3 = o.writerFor(new TypeReference>(){}).writeValueAsString(m2); + javaMap = o.readValue(t3, Map.class); + assertEquals(2, javaMap.size()); + + String t4 = o.writerFor(new TypeReference>(){}).writeValueAsString(m2); + javaMap = o.readValue(t4, Map.class); + assertEquals(2, javaMap.size()); + } + + public void testEnumKey() throws Exception + { + final TypeReference> type = new TypeReference>() {}; + final Multimap map = TreeMultimap.create(); + + map.put(MyEnum.YAY, 5); + map.put(MyEnum.BOO, 2); + + final String serializedForm = MAPPER.writerFor(type).writeValueAsString(map); + + assertEquals(serializedForm, MAPPER.writeValueAsString(map)); + assertEquals(map, MAPPER.readValue(serializedForm, type)); + } + + // [Issue#41] + public void testEmptyMapExclusion() throws Exception + { + String json = MAPPER.writeValueAsString(new MultiMapWrapper()); + assertEquals("{}", json); + } + + public void testNullHandling() throws Exception + { + Multimap input = ArrayListMultimap.create(); + input.put("empty", null); + String json = MAPPER.writeValueAsString(input); + assertEquals(aposToQuotes("{'empty':[null]}"), json); + } + + /* + /********************************************************************** + /* Unit tests for set-based multimaps + /********************************************************************** + */ + + /* + public void testTreeMultimap() throws IOException { + } + + public void testForwardingSortedSetMultimap() throws IOException { + + } + */ + + public void testImmutableSetMultimap() throws IOException { + SetMultimap map = + _verifyMultiMapRead(new TypeReference>() { + }); + assertTrue(map instanceof ImmutableSetMultimap); + } + + public void testHashMultimap() throws IOException { + SetMultimap map = + _verifyMultiMapRead(new TypeReference>() { + }); + assertTrue(map instanceof HashMultimap); + } + + public void testLinkedHashMultimap() throws IOException { + SetMultimap map = + _verifyMultiMapRead(new TypeReference>() { + }); + assertTrue(map instanceof LinkedHashMultimap); + } + + /* + public void testForwardingSetMultimap() { + } + */ + + private SetMultimap _verifyMultiMapRead(TypeReference type) + throws IOException + { + SetMultimap map = MAPPER.readValue(STRING_STRING_MULTIMAP, type); + assertEquals(3, map.size()); + assertTrue(map.containsEntry("first", "abc")); + assertTrue(map.containsEntry("first", "foo")); + assertTrue(map.containsEntry("second", "bar")); + return map; + } + + /* + /********************************************************************** + /* Unit tests for list-based multimaps + /********************************************************************** + */ + + public void testArrayListMultimap() throws IOException { + ListMultimap map = + listBasedHelper(new TypeReference>() { + }); + assertTrue(map instanceof ArrayListMultimap); + } + + public void testLinkedListMultimap() throws IOException { + ListMultimap map = + listBasedHelper(new TypeReference>() { + }); + assertTrue(map instanceof LinkedListMultimap); + } + + public void testMultimapWithIgnores() throws IOException { + assertEquals("{\"map\":{\"a\":[\"foo\"]}}", + MAPPER.writeValueAsString(new MultiMapWithIgnores())); + } + + private ListMultimap listBasedHelper(TypeReference type) throws IOException { + ListMultimap map = MAPPER.readValue(STRING_STRING_MULTIMAP, type); + assertEquals(4, map.size()); + assertTrue(map.remove("first", "abc")); + assertTrue(map.containsEntry("first", "abc")); + assertTrue(map.containsEntry("first", "foo")); + assertTrue(map.containsEntry("second", "bar")); + return map; + } + + public void testIssue67() throws IOException + { + ImmutableSetMultimap map = MAPPER.readValue( + "{\"d\":[1,2],\"c\":[3,4],\"b\":[5,6],\"a\":[7,8]}", + new TypeReference>() {}); + assertTrue(map instanceof ImmutableSetMultimap); + assertEquals(8, map.size()); + Iterator> iterator = map.entries().iterator(); + assertEquals(Maps.immutableEntry("d", 1), iterator.next()); + assertEquals(Maps.immutableEntry("d", 2), iterator.next()); + assertEquals(Maps.immutableEntry("c", 3), iterator.next()); + assertEquals(Maps.immutableEntry("c", 4), iterator.next()); + assertEquals(Maps.immutableEntry("b", 5), iterator.next()); + assertEquals(Maps.immutableEntry("b", 6), iterator.next()); + assertEquals(Maps.immutableEntry("a", 7), iterator.next()); + assertEquals(Maps.immutableEntry("a", 8), iterator.next()); + } + + public void testPolymorphicValue() throws IOException { + ImmutableMultimapWrapper input = new ImmutableMultimapWrapper(ImmutableMultimap.of("add", new AddOp(3, 2), "mul", new MulOp(4, 6))); + + String json = MAPPER.writeValueAsString(input); + + ImmutableMultimapWrapper output = MAPPER.readValue(json, ImmutableMultimapWrapper.class); + assertEquals(input, output); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultisets.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultisets.java new file mode 100644 index 00000000..9525723c --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestMultisets.java @@ -0,0 +1,125 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.core.type.TypeReference; + +import com.fasterxml.jackson.databind.*; + +import com.google.common.collect.*; + +/** + * Unit tests to verify handling of various {@link Multiset}s. + * + * @author tsaloranta + */ +public class TestMultisets extends ModuleTestBase +{ + /* + /********************************************************************** + /* Unit tests for verifying handling in absence of module registration + /********************************************************************** + */ + + /** + * Multi-sets can actually be serialized as regular collections, without + * problems. + */ + public void testWithoutSerializers() throws Exception + { + + ObjectMapper mapper = new ObjectMapper(); + Multiset set = LinkedHashMultiset.create(); + // hash-based multi-sets actually keeps 'same' instances together, otherwise insertion-ordered: + set.add("abc"); + set.add("foo"); + set.add("abc"); + String json = mapper.writeValueAsString(set); + assertEquals("[\"abc\",\"abc\",\"foo\"]", json); + } + + public void testWithoutDeserializers() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + try { + /*Multiset set =*/ mapper.readValue("[\"abc\",\"abc\",\"foo\"]", + new TypeReference>() { }); + fail("Should have failed"); + } catch (JsonMappingException e) { + verifyException(e, "can not find a deserializer"); + } + } + + /* + /********************************************************************** + /* Unit tests for actual registered module + /********************************************************************** + */ + + final ObjectMapper MAPPER = mapperWithModule(); + + public void testDefaultMultiset() throws Exception + { + Multiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testDefaultSortedMultiset() throws Exception { + SortedMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testLinkedHashMultiset() throws Exception { + LinkedHashMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testHashMultiset() throws Exception { + HashMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testTreeMultiset() throws Exception { + TreeMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testImmutableMultiset() throws Exception { + ImmutableMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testImmutableSortedMultiset() throws Exception { + ImmutableSortedMultiset set = MAPPER.readValue("[\"abc\",\"abc\",\"foo\"]", new TypeReference>() { }); + assertEquals(3, set.size()); + assertEquals(1, set.count("foo")); + assertEquals(2, set.count("abc")); + assertEquals(0, set.count("bar")); + } + + public void testFromSingle() throws Exception + { + ObjectMapper mapper = mapperWithModule() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + Multiset set = mapper.readValue("\"abc\"", + new TypeReference>() { }); + assertEquals(1, set.size()); + assertTrue(set.contains("abc")); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestRange.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestRange.java new file mode 100644 index 00000000..873761f6 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestRange.java @@ -0,0 +1,252 @@ +package com.fasterxml.jackson.datatype.guava; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.guava.deser.util.RangeFactory; + +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +import java.io.IOException; + +/** + * Unit tests to verify serialization of Guava {@link Range}s. + */ +public class TestRange extends ModuleTestBase { + + private final ObjectMapper MAPPER = mapperWithModule(); + + protected static class Untyped + { + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + public Object range; + + public Untyped() { } + public Untyped(Range r) { range = r; } + } + + static class Wrapped { + public Range r; + + public Wrapped() { } + public Wrapped(Range r) { this.r = r; } + } + + /** + * This test is present so that we know if either Jackson's handling of Range + * or Guava's implementation of Range changes. + * @throws Exception + */ + public void testSerializationWithoutModule() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + Range range = RangeFactory.closed(1, 10); + String json = mapper.writeValueAsString(range); + assertEquals("{\"empty\":false}", json); + } + + public void testSerialization() throws Exception + { + testSerialization(MAPPER, RangeFactory.open(1, 10)); + testSerialization(MAPPER, RangeFactory.openClosed(1, 10)); + testSerialization(MAPPER, RangeFactory.closedOpen(1, 10)); + testSerialization(MAPPER, RangeFactory.closed(1, 10)); + testSerialization(MAPPER, RangeFactory.atLeast(1)); + testSerialization(MAPPER, RangeFactory.greaterThan(1)); + testSerialization(MAPPER, RangeFactory.atMost(10)); + testSerialization(MAPPER, RangeFactory.lessThan(10)); + testSerialization(MAPPER, RangeFactory.all()); + testSerialization(MAPPER, RangeFactory.singleton(1)); + } + + public void testWrappedSerialization() throws Exception + { + testSerializationWrapped(MAPPER, RangeFactory.open(1, 10)); + testSerializationWrapped(MAPPER, RangeFactory.openClosed(1, 10)); + testSerializationWrapped(MAPPER, RangeFactory.closedOpen(1, 10)); + testSerializationWrapped(MAPPER, RangeFactory.closed(1, 10)); + testSerializationWrapped(MAPPER, RangeFactory.atLeast(1)); + testSerializationWrapped(MAPPER, RangeFactory.greaterThan(1)); + testSerializationWrapped(MAPPER, RangeFactory.atMost(10)); + testSerializationWrapped(MAPPER, RangeFactory.lessThan(10)); + testSerializationWrapped(MAPPER, RangeFactory.singleton(1)); + } + + public void testDeserialization() throws Exception + { + String json = MAPPER.writeValueAsString(RangeFactory.open(1, 10)); + @SuppressWarnings("unchecked") + Range r = (Range) MAPPER.readValue(json, Range.class); + assertNotNull(r); + assertEquals(Integer.valueOf(1), r.lowerEndpoint()); + assertEquals(Integer.valueOf(10), r.upperEndpoint()); + } + + private void testSerialization(ObjectMapper objectMapper, Range range) throws IOException + { + String json = objectMapper.writeValueAsString(range); + Range rangeClone = objectMapper.readValue(json, Range.class); + assertEquals(rangeClone, range); + } + + private void testSerializationWrapped(ObjectMapper objectMapper, Range range) throws IOException + { + String json = objectMapper.writeValueAsString(new Wrapped(range)); + Wrapped result = objectMapper.readValue(json, Wrapped.class); + assertEquals(range, result.r); + } + + public void testUntyped() throws Exception + { + String json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(new Untyped(RangeFactory.open(1, 10))); + Untyped out = MAPPER.readValue(json, Untyped.class); + assertNotNull(out); + assertEquals(Range.class, out.range.getClass()); + } + + public void testDefaultBoundTypeNoBoundTypeInformed() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"upperEndpoint\": 3}"; + + try { + MAPPER.readValue(json, Range.class); + fail("Should have failed"); + } catch (JsonMappingException e) { + verifyException(e, "'lowerEndpoint' field found, but not 'lowerBoundType'"); + } + } + + public void testDefaultBoundTypeNoBoundTypeInformedWithClosedConfigured() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"upperEndpoint\": 3}"; + + GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED); + ObjectMapper mapper = new ObjectMapper().registerModule(mod); + + @SuppressWarnings("unchecked") + Range r = (Range) mapper.readValue(json, Range.class); + + assertEquals(Integer.valueOf(2), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + assertEquals(BoundType.CLOSED, r.lowerBoundType()); + assertEquals(BoundType.CLOSED, r.upperBoundType()); + } + + public void testDefaultBoundTypeOnlyLowerBoundTypeInformed() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"lowerBoundType\": \"OPEN\", \"upperEndpoint\": 3}"; + + try { + MAPPER.readValue(json, Range.class); + fail("Should have failed"); + } catch (JsonMappingException e) { + verifyException(e, "'upperEndpoint' field found, but not 'upperBoundType'"); + } + } + + public void testDefaultBoundTypeOnlyLowerBoundTypeInformedWithClosedConfigured() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"lowerBoundType\": \"OPEN\", \"upperEndpoint\": 3}"; + + GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED); + ObjectMapper mapper = new ObjectMapper().registerModule(mod); + + @SuppressWarnings("unchecked") + Range r = (Range) mapper.readValue(json, Range.class); + + assertEquals(Integer.valueOf(2), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + assertEquals(BoundType.OPEN, r.lowerBoundType()); + assertEquals(BoundType.CLOSED, r.upperBoundType()); + } + + public void testDefaultBoundTypeOnlyUpperBoundTypeInformed() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"upperEndpoint\": 3, \"upperBoundType\": \"OPEN\"}"; + + try { + MAPPER.readValue(json, Range.class); + fail("Should have failed"); + } catch (JsonMappingException e) { + verifyException(e, "'lowerEndpoint' field found, but not 'lowerBoundType'"); + } + } + + public void testDefaultBoundTypeOnlyUpperBoundTypeInformedWithClosedConfigured() throws Exception + { + String json = "{\"lowerEndpoint\": 1, \"upperEndpoint\": 3, \"upperBoundType\": \"OPEN\"}"; + + GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED); + ObjectMapper mapper = new ObjectMapper().registerModule(mod); + + @SuppressWarnings("unchecked") + Range r = (Range) mapper.readValue(json, Range.class); + + assertEquals(Integer.valueOf(1), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + assertEquals(BoundType.CLOSED, r.lowerBoundType()); + assertEquals(BoundType.OPEN, r.upperBoundType()); + } + + public void testDefaultBoundTypeBothBoundTypesOpen() throws Exception + { + String json = "{\"lowerEndpoint\": 2, \"lowerBoundType\": \"OPEN\", \"upperEndpoint\": 3, \"upperBoundType\": \"OPEN\"}"; + @SuppressWarnings("unchecked") + Range r = (Range) MAPPER.readValue(json, Range.class); + + assertEquals(Integer.valueOf(2), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + + assertEquals(BoundType.OPEN, r.lowerBoundType()); + assertEquals(BoundType.OPEN, r.upperBoundType()); + } + + public void testDefaultBoundTypeBothBoundTypesOpenWithClosedConfigured() throws Exception + { + String json = "{\"lowerEndpoint\": 1, \"lowerBoundType\": \"OPEN\", \"upperEndpoint\": 3, \"upperBoundType\": \"OPEN\"}"; + + GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED); + ObjectMapper mapper = new ObjectMapper().registerModule(mod); + + @SuppressWarnings("unchecked") + Range r = (Range) mapper.readValue(json, Range.class); + + assertEquals(Integer.valueOf(1), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + + assertEquals(BoundType.OPEN, r.lowerBoundType()); + assertEquals(BoundType.OPEN, r.upperBoundType()); + } + + public void testDefaultBoundTypeBothBoundTypesClosed() throws Exception + { + String json = "{\"lowerEndpoint\": 1, \"lowerBoundType\": \"CLOSED\", \"upperEndpoint\": 3, \"upperBoundType\": \"CLOSED\"}"; + @SuppressWarnings("unchecked") + Range r = (Range) MAPPER.readValue(json, Range.class); + + assertEquals(Integer.valueOf(1), r.lowerEndpoint()); + assertEquals(Integer.valueOf(3), r.upperEndpoint()); + + assertEquals(BoundType.CLOSED, r.lowerBoundType()); + assertEquals(BoundType.CLOSED, r.upperBoundType()); + } + + public void testDefaultBoundTypeBothBoundTypesClosedWithOpenConfigured() throws Exception + { + String json = "{\"lowerEndpoint\": 12, \"lowerBoundType\": \"CLOSED\", \"upperEndpoint\": 33, \"upperBoundType\": \"CLOSED\"}"; + + GuavaModule mod = new GuavaModule().defaultBoundType(BoundType.CLOSED); + ObjectMapper mapper = new ObjectMapper().registerModule(mod); + + @SuppressWarnings("unchecked") + Range r = (Range) mapper.readValue(json, Range.class); + + assertEquals(Integer.valueOf(12), r.lowerEndpoint()); + assertEquals(Integer.valueOf(33), r.upperEndpoint()); + + assertEquals(BoundType.CLOSED, r.lowerBoundType()); + assertEquals(BoundType.CLOSED, r.upperBoundType()); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestVersions.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestVersions.java new file mode 100644 index 00000000..d8fbe2ec --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/TestVersions.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.datatype.guava; + +import java.io.*; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.datatype.guava.PackageVersion; + +public class TestVersions extends ModuleTestBase +{ + public void testMapperVersions() throws IOException + { + GuavaModule module = new GuavaModule(); + assertVersion(module); + } + + public void testPackageVersion() + { + assertEquals(PackageVersion.VERSION, + VersionUtil.versionFor(GuavaModule.class)); + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private void assertVersion(Versioned vers) + { + final Version v = vers.version(); + assertFalse("Should find version information (got "+v+")", v.isUnknownVersion()); + assertEquals(PackageVersion.VERSION, v); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/failing/OptionalUnwrappedTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/failing/OptionalUnwrappedTest.java new file mode 100644 index 00000000..827963a6 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/failing/OptionalUnwrappedTest.java @@ -0,0 +1,39 @@ +package com.fasterxml.jackson.datatype.guava.failing; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; + +import com.google.common.base.Optional; + +import com.fasterxml.jackson.datatype.guava.*; + +/** + * Unit test for remaining part of #64. + */ +public class OptionalUnwrappedTest extends ModuleTestBase +{ + static class Child { + public String name = "Bob"; + } + + static class Parent { + private Child child = new Child(); + + @JsonUnwrapped + public Child getChild() { return child; } + } + + static class OptionalParent { + @JsonUnwrapped(prefix="XX.") + public Optional child = Optional.of(new Child()); + } + + // Test for "old" settings (2.5 and earlier only option; available on later too) + public void testUntypedWithNullEqOptionals() throws Exception + { + final ObjectMapper mapper = mapperWithModule(true); + String jsonExp = aposToQuotes("{'XX.name':'Bob'}"); + String jsonAct = mapper.writeValueAsString(new OptionalParent()); + assertEquals(jsonExp, jsonAct); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalBasicTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalBasicTest.java new file mode 100644 index 00000000..511f8797 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalBasicTest.java @@ -0,0 +1,321 @@ +package com.fasterxml.jackson.datatype.guava.optional; + +import java.io.IOException; +import java.util.*; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.datatype.guava.ModuleTestBase; + +import com.google.common.base.Optional; + +public class OptionalBasicTest extends ModuleTestBase +{ + public static final class OptionalData { + public Optional myString; + } + + public static final class OptionalGenericData{ + public Optional myData; + } + + @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class) + public static class Unit + { +// @JsonIdentityReference(alwaysAsId=true) + public Optional baseUnit; + + public Unit() { } + public Unit(Optional u) { baseUnit = u; } + + public void link(Unit u) { + baseUnit = Optional.of(u); + } + } + + // To test handling of polymorphic value types + + public static class Container { + public Optional contained; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY) + @JsonSubTypes({ + @JsonSubTypes.Type(name = "ContainedImpl", value = ContainedImpl.class), + }) + public static interface Contained { } + + public static class ContainedImpl implements Contained { } + + static class CaseChangingStringWrapper { + @JsonSerialize(contentUsing=UpperCasingSerializer.class) + @JsonDeserialize(contentUsing=LowerCasingDeserializer.class) + public Optional value; + + CaseChangingStringWrapper() { } + public CaseChangingStringWrapper(String s) { value = Optional.of(s); } + } + + @SuppressWarnings("serial") + public static class UpperCasingSerializer extends StdScalarSerializer + { + public UpperCasingSerializer() { super(String.class); } + + @Override + public void serialize(String value, JsonGenerator gen, + SerializerProvider provider) throws IOException { + gen.writeString(value.toUpperCase()); + } + } + + @SuppressWarnings("serial") + public static class LowerCasingDeserializer extends StdScalarDeserializer + { + public LowerCasingDeserializer() { super(String.class); } + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + return p.getText().toLowerCase(); + } + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + private final ObjectMapper MAPPER = mapperWithModule(); + + public void testOptionalTypeResolution() throws Exception + { + // With 2.6, we need to recognize it as ReferenceType + JavaType t = MAPPER.constructType(Optional.class); + assertNotNull(t); + assertEquals(Optional.class, t.getRawClass()); + assertTrue(t.isReferenceType()); + } + + public void testDeserAbsent() throws Exception { + Optional value = MAPPER.readValue("null", new TypeReference>() {}); + assertFalse(value.isPresent()); + } + + public void testDeserSimpleString() throws Exception{ + Optional value = MAPPER.readValue("\"simpleString\"", new TypeReference>() {}); + assertTrue(value.isPresent()); + assertEquals("simpleString", value.get()); + } + + public void testDeserInsideObject() throws Exception { + OptionalData data = MAPPER.readValue("{\"myString\":\"simpleString\"}", OptionalData.class); + assertTrue(data.myString.isPresent()); + assertEquals("simpleString", data.myString.get()); + } + + public void testDeserComplexObject() throws Exception { + TypeReference> type = new TypeReference>() {}; + Optional data = MAPPER.readValue("{\"myString\":\"simpleString\"}", type); + assertTrue(data.isPresent()); + assertTrue(data.get().myString.isPresent()); + assertEquals("simpleString", data.get().myString.get()); + } + + public void testDeserGeneric() throws Exception { + TypeReference>> type = new TypeReference>>() {}; + Optional> data = MAPPER.readValue("{\"myData\":\"simpleString\"}", type); + assertTrue(data.isPresent()); + assertTrue(data.get().myData.isPresent()); + assertEquals("simpleString", data.get().myData.get()); + } + + public void testSerAbsent() throws Exception { + String value = MAPPER.writeValueAsString(Optional.absent()); + assertEquals("null", value); + } + + public void testSerSimpleString() throws Exception { + String value = MAPPER.writeValueAsString(Optional.of("simpleString")); + assertEquals("\"simpleString\"", value); + } + + public void testSerInsideObject() throws Exception { + OptionalData data = new OptionalData(); + data.myString = Optional.of("simpleString"); + String value = MAPPER.writeValueAsString(data); + assertEquals("{\"myString\":\"simpleString\"}", value); + } + + public void testSerComplexObject() throws Exception { + OptionalData data = new OptionalData(); + data.myString = Optional.of("simpleString"); + String value = MAPPER.writeValueAsString(Optional.of(data)); + assertEquals("{\"myString\":\"simpleString\"}", value); + } + + public void testSerGeneric() throws Exception { + OptionalGenericData data = new OptionalGenericData(); + data.myData = Optional.of("simpleString"); + String value = MAPPER.writeValueAsString(Optional.of(data)); + assertEquals("{\"myData\":\"simpleString\"}", value); + } + + public void testSerNonNull() throws Exception { + OptionalData data = new OptionalData(); + data.myString = Optional.absent(); + // NOTE: pass 'true' to ensure "legacy" setting + String value = mapperWithModule(true) + .setSerializationInclusion(JsonInclude.Include.NON_NULL).writeValueAsString(data); + assertEquals("{}", value); + } + + public void testSerOptDefault() throws Exception { + OptionalData data = new OptionalData(); + data.myString = Optional.absent(); + String value = mapperWithModule().setSerializationInclusion(JsonInclude.Include.ALWAYS).writeValueAsString(data); + assertEquals("{\"myString\":null}", value); + } + + public void testSerOptNull() throws Exception { + OptionalData data = new OptionalData(); + data.myString = null; + String value = mapperWithModule().setSerializationInclusion(JsonInclude.Include.NON_NULL).writeValueAsString(data); + assertEquals("{}", value); + } + + // for [dataformat-guava#66] + public void testSerOptDisableAsNull() throws Exception { + final OptionalData data = new OptionalData(); + data.myString = Optional.absent(); + + GuavaModule mod = new GuavaModule().configureAbsentsAsNulls(false); + ObjectMapper mapper = new ObjectMapper() + .registerModule(mod) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + assertEquals("{\"myString\":null}", mapper.writeValueAsString(data)); + + // but do exclude with NON_EMPTY + mapper = new ObjectMapper() + .registerModule(mod) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + assertEquals("{}", mapper.writeValueAsString(data)); + + // and with new (2.6) NON_ABSENT + mapper = new ObjectMapper() + .registerModule(mod) + .setSerializationInclusion(JsonInclude.Include.NON_ABSENT); + assertEquals("{}", mapper.writeValueAsString(data)); + } + + public void testSerOptNonEmpty() throws Exception { + OptionalData data = new OptionalData(); + data.myString = null; + String value = mapperWithModule().setSerializationInclusion(JsonInclude.Include.NON_EMPTY).writeValueAsString(data); + assertEquals("{}", value); + } + + public void testSerOptNonDefault() throws Exception { + OptionalData data = new OptionalData(); + data.myString = null; + String value = mapperWithModule().setSerializationInclusion(JsonInclude.Include.NON_DEFAULT).writeValueAsString(data); + assertEquals("{}", value); + } + + public void testWithTypingEnabled() throws Exception + { + final ObjectMapper objectMapper = mapperWithModule(); + // ENABLE TYPING + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); + + final OptionalData myData = new OptionalData(); + myData.myString = Optional.fromNullable("abc"); + + final String json = objectMapper.writeValueAsString(myData); + final OptionalData deserializedMyData = objectMapper.readValue(json, OptionalData.class); + assertEquals(myData.myString, deserializedMyData.myString); + } + + // [datatype-guava#17] + public void testObjectId() throws Exception + { + final Unit input = new Unit(); + input.link(input); + String json = MAPPER.writeValueAsString(input); + Unit result = MAPPER.readValue(json, Unit.class); + assertNotNull(result); + assertNotNull(result.baseUnit); + assertTrue(result.baseUnit.isPresent()); + Unit base = result.baseUnit.get(); + assertSame(result, base); + } + + // [Issue#37] + public void testOptionalCollection() throws Exception { + ObjectMapper mapper = new ObjectMapper().registerModule(new GuavaModule()); + + TypeReference>> typeReference = + new TypeReference>>() {}; + + List> list = new ArrayList>(); + list.add(Optional.of("2014-1-22")); + list.add(Optional.absent()); + list.add(Optional.of("2014-1-23")); + + String str = mapper.writeValueAsString(list); + assertEquals("[\"2014-1-22\",null,\"2014-1-23\"]", str); + + List> result = mapper.readValue(str, typeReference); + assertEquals(list.size(), result.size()); + for (int i = 0; i < list.size(); ++i) { + assertEquals("Entry #"+i, list.get(i), result.get(i)); + } + } + + // [datatype-guava#48] + public void testDeserNull() throws Exception { + Optional value = MAPPER.readValue("\"\"", new TypeReference>() {}); + assertFalse(value.isPresent()); + } + + // [datatype-guava#81] + public void testPolymorphic() throws Exception + { + final Container dto = new Container(); + dto.contained = Optional.of((Contained) new ContainedImpl()); + + final String json = MAPPER.writeValueAsString(dto); + + final Container fromJson = MAPPER.readValue(json, Container.class); + assertNotNull(fromJson.contained); + assertTrue(fromJson.contained.isPresent()); + assertSame(ContainedImpl.class, fromJson.contained.get().getClass()); + } + + public void testWithCustomDeserializer() throws Exception + { + CaseChangingStringWrapper w = MAPPER.readValue(aposToQuotes("{'value':'FoobaR'}"), + CaseChangingStringWrapper.class); + assertEquals("foobar", w.value.get()); + } + + public void testCustomSerializer() throws Exception + { + final String VALUE = "fooBAR"; + String json = MAPPER.writeValueAsString(new CaseChangingStringWrapper(VALUE)); + assertEquals(json, aposToQuotes("{'value':'FOOBAR'}")); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalSchema83Test.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalSchema83Test.java new file mode 100644 index 00000000..71e4e47d --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalSchema83Test.java @@ -0,0 +1,131 @@ +package com.fasterxml.jackson.datatype.guava.optional; + +import java.util.*; + +import com.google.common.base.Optional; +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsonFormatVisitors.*; +import com.fasterxml.jackson.datatype.guava.ModuleTestBase; + +public class OptionalSchema83Test + extends ModuleTestBase +{ + static class TopLevel { + @JsonProperty("values") + public Optional> values; + } + + static class ValueHolder { + @JsonProperty("value") + public String value; + } + + static class CollectionHolder { + @JsonProperty("data") + public Collection data; + } + + static class VisitorWrapper implements JsonFormatVisitorWrapper { + private SerializerProvider serializerProvider; + private final String baseName; + private final Set traversedProperties; + + public VisitorWrapper(SerializerProvider serializerProvider, String baseName, Set traversedProperties) { + this.serializerProvider = serializerProvider; + this.baseName = baseName; + this.traversedProperties = traversedProperties; + } + + private VisitorWrapper createSubtraverser(String bn) { + return new VisitorWrapper(getProvider(), bn, traversedProperties); + } + + public Set getTraversedProperties() { + return traversedProperties; + } + + @Override + public JsonObjectFormatVisitor expectObjectFormat(JavaType type) throws JsonMappingException { + return new JsonObjectFormatVisitor.Base(serializerProvider) { + @Override + public void property(BeanProperty prop) throws JsonMappingException { + anyProperty(prop); + } + + @Override + public void optionalProperty(BeanProperty prop) throws JsonMappingException { + anyProperty(prop); + } + + private void anyProperty(BeanProperty prop) throws JsonMappingException { + final String propertyName = prop.getFullName().toString(); + traversedProperties.add(baseName + propertyName); + serializerProvider.findValueSerializer(prop.getType(), prop) + .acceptJsonFormatVisitor(createSubtraverser(baseName + propertyName + "."), prop.getType()); + } + }; + } + + @Override + public JsonArrayFormatVisitor expectArrayFormat(JavaType type) throws JsonMappingException { + serializerProvider.findValueSerializer(type.getContentType()) + .acceptJsonFormatVisitor(createSubtraverser(baseName), type.getContentType()); + return new JsonArrayFormatVisitor.Base(serializerProvider); + } + + @Override + public JsonStringFormatVisitor expectStringFormat(JavaType type) throws JsonMappingException { + return new JsonStringFormatVisitor.Base(); + } + + @Override + public JsonNumberFormatVisitor expectNumberFormat(JavaType type) throws JsonMappingException { + return new JsonNumberFormatVisitor.Base(); + } + + @Override + public JsonIntegerFormatVisitor expectIntegerFormat(JavaType type) throws JsonMappingException { + return new JsonIntegerFormatVisitor.Base(); + } + + @Override + public JsonBooleanFormatVisitor expectBooleanFormat(JavaType type) throws JsonMappingException { + return new JsonBooleanFormatVisitor.Base(); + } + + @Override + public JsonNullFormatVisitor expectNullFormat(JavaType type) throws JsonMappingException { + return new JsonNullFormatVisitor.Base(); + } + + @Override + public JsonAnyFormatVisitor expectAnyFormat(JavaType type) throws JsonMappingException { + return new JsonAnyFormatVisitor.Base(); + } + + @Override + public JsonMapFormatVisitor expectMapFormat(JavaType type) throws JsonMappingException { + return new JsonMapFormatVisitor.Base(serializerProvider); + } + + @Override + public SerializerProvider getProvider() { + return serializerProvider; + } + + @Override + public void setProvider(SerializerProvider provider) { + this.serializerProvider = provider; + } + } + + public void testOptionalTypeSchema83() throws Exception { + VisitorWrapper wrapper = new VisitorWrapper(null, "", new HashSet()); + mapperWithModule() + .acceptJsonFormatVisitor(TopLevel.class, wrapper); + Set properties = wrapper.getTraversedProperties(); + + assertTrue(properties.contains("values.data.value")); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalUnwrappedTest.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalUnwrappedTest.java new file mode 100644 index 00000000..af6db39a --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/OptionalUnwrappedTest.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.datatype.guava.optional; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.datatype.guava.ModuleTestBase; +import com.google.common.base.Optional; + +/** + * Unit test for #64, in new mode. + */ +public class OptionalUnwrappedTest extends ModuleTestBase +{ + static class Child { + public String name = "Bob"; + } + + static class Parent { + private Child child = new Child(); + + @JsonUnwrapped + public Child getChild() { return child; } + } + + static class OptionalParent { + @JsonUnwrapped(prefix="XX.") + public Optional child = Optional.of(new Child()); + } + + // Test for "new" settings of absent != nulls, available on 2.6 and later + public void testUntypedWithOptionalsNotNulls() throws Exception + { + final ObjectMapper mapper = mapperWithModule(false); + String jsonExp = aposToQuotes("{'XX.name':'Bob'}"); + String jsonAct = mapper.writeValueAsString(new OptionalParent()); + assertEquals(jsonExp, jsonAct); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/TestOptionalWithPolymorphic.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/TestOptionalWithPolymorphic.java new file mode 100644 index 00000000..4f38d1b9 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/optional/TestOptionalWithPolymorphic.java @@ -0,0 +1,123 @@ +package com.fasterxml.jackson.datatype.guava.optional; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.guava.ModuleTestBase; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; + +public class TestOptionalWithPolymorphic extends ModuleTestBase +{ + static class ContainerA { + @JsonProperty private Optional name = Optional.absent(); + @JsonProperty private Optional strategy = Optional.absent(); + } + + static class ContainerB { + @JsonProperty private Optional name = Optional.absent(); + @JsonProperty private Strategy strategy = null; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(name = "Foo", value = Foo.class), + @JsonSubTypes.Type(name = "Bar", value = Bar.class), + @JsonSubTypes.Type(name = "Baz", value = Baz.class) + }) + interface Strategy { + } + + static class Foo implements Strategy { + @JsonProperty private final int foo; + @JsonCreator Foo(@JsonProperty("foo") int foo) { + this.foo = foo; + } + } + + static class Bar implements Strategy { + @JsonProperty private final boolean bar; + @JsonCreator Bar(@JsonProperty("bar") boolean bar) { + this.bar = bar; + } + } + + static class Baz implements Strategy { + @JsonProperty private final String baz; + @JsonCreator Baz(@JsonProperty("baz") String baz) { + this.baz = baz; + } + } + + static class AbstractOptional { + @JsonDeserialize(contentAs=Integer.class) + public Optional value; + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + final ObjectMapper MAPPER = mapperWithModule(); + + public void testOptionalMapsFoo() throws Exception { + + ImmutableMap foo = ImmutableMap.builder() + .put("name", "foo strategy") + .put("strategy", ImmutableMap.builder() + .put("type", "Foo") + .put("foo", 42) + .build()) + .build(); + _test(MAPPER, foo); + } + + public void testOptionalMapsBar() throws Exception { + + ImmutableMap bar = ImmutableMap.builder() + .put("name", "bar strategy") + .put("strategy", ImmutableMap.builder() + .put("type", "Bar") + .put("bar", true) + .build()) + .build(); + _test(MAPPER, bar); + } + + public void testOptionalMapsBaz() throws Exception { + ImmutableMap baz = ImmutableMap.builder() + .put("name", "baz strategy") + .put("strategy", ImmutableMap.builder() + .put("type", "Baz") + .put("baz", "hello world!") + .build()) + .build(); + _test(MAPPER, baz); + } + + public void testOptionalWithTypeAnnotation() throws Exception + { + AbstractOptional result = MAPPER.readValue("{\"value\" : 5}", + AbstractOptional.class); + assertNotNull(result); + assertNotNull(result.value); + Object ob = result.value.get(); + assertEquals(Integer.class, ob.getClass()); + assertEquals(Integer.valueOf(5), ob); + } + + private void _test(ObjectMapper m, Map map) throws Exception + { + String json = m.writeValueAsString(map); + + ContainerA objA = m.readValue(json, ContainerA.class); + assertNotNull(objA); + + ContainerB objB = m.readValue(json, ContainerB.class); + assertNotNull(objB); + } +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/AddOp.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/AddOp.java new file mode 100644 index 00000000..167a78ee --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/AddOp.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.datatype.guava.pojo; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * + * @author Marcin Kamionowski + */ +public class AddOp implements MathOp { + + private final int left; + private final int right; + + @JsonCreator + public AddOp( + @JsonProperty("left") int left, + @JsonProperty("right") int right) { + this.left = left; + this.right = right; + } + + public int getLeft() { + return left; + } + + public int getRight() { + return right; + } + + public int value() { + return left + right; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 47 * hash + this.left; + hash = 47 * hash + this.right; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final AddOp other = (AddOp) obj; + if (this.left != other.left) { + return false; + } + return this.right == other.right; + } + +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MathOp.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MathOp.java new file mode 100644 index 00000000..afe3b7a4 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MathOp.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.datatype.guava.pojo; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * + * @author Marcin Kamionowski + */ +@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="_t") +@JsonSubTypes({ + @Type(name="add", value=AddOp.class), + @Type(name="mul", value=MulOp.class)}) +public interface MathOp { + +} diff --git a/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MulOp.java b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MulOp.java new file mode 100644 index 00000000..2fa9b495 --- /dev/null +++ b/guava/src/test/java/com/fasterxml/jackson/datatype/guava/pojo/MulOp.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.datatype.guava.pojo; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * + * @author Marcin Kamionowski + */ +public class MulOp implements MathOp { + + private final int left; + private final int right; + + @JsonCreator + public MulOp( + @JsonProperty("left") int left, + @JsonProperty("right") int right) { + this.left = left; + this.right = right; + } + + public int getLeft() { + return left; + } + + public int getRight() { + return right; + } + + public int value() { + return left * right; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 67 * hash + this.left; + hash = 67 * hash + this.right; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final MulOp other = (MulOp) obj; + if (this.left != other.left) { + return false; + } + return this.right == other.right; + } + +} diff --git a/pom.xml b/pom.xml index 03ae4bb1..f3b0bb4f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,13 +7,14 @@ com.fasterxml.jackson.datatype jackson-datatypes-collections - Jackson datatypes: Collections + Jackson datatypes: collections 2.7.4-SNAPSHOT pom Parent pom for Jackson Collection datatype modules. + guava hppc pcollections @@ -28,7 +29,7 @@ UTF-8 - 2.7.3 + 2.7.4-SNAPSHOT 1.6 @@ -36,4 +37,19 @@ + + + + com.fasterxml.jackson.core + jackson-core + ${version.jackson.core} + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson.core} + + + +