From 0fb4d9672ef9e935d25708eeb99670f65514bad0 Mon Sep 17 00:00:00 2001 From: Jon Peterson Date: Sun, 31 Jul 2016 21:43:02 -0400 Subject: [PATCH] Overhauled and added documentation. --- build.gradle | 12 +- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 46 +++-- gradlew.bat | 8 +- .../module/versioning/JsonVersionedModel.java | 62 ++++++ .../module/versioning/VersionedModel.java | 10 - .../versioning/VersionedModelConverter.java | 17 ++ .../VersionedModelDataTransformer.java | 13 -- .../VersionedModelDeserializer.java | 61 +++--- .../versioning/VersionedModelSerializer.java | 51 +++++ .../module/versioning/VersioningModule.java | 54 +++-- .../versioning/VersioningModuleTest.groovy | 195 +++++++++++++++--- 14 files changed, 396 insertions(+), 138 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java delete mode 100644 src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModel.java create mode 100644 src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java delete mode 100644 src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDataTransformer.java create mode 100644 src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java diff --git a/build.gradle b/build.gradle index 127156d..582b4b2 100644 --- a/build.gradle +++ b/build.gradle @@ -2,16 +2,13 @@ buildscript { ext { // external dependency versions groovyVersion = '2.4.5' - gradleVersion = '2.9' - jacksonVersion = '2.6.7' - slf4jVersion = '1.7.12' + gradleVersion = '2.14.1' + jacksonVersion = '[2.2,)' spockVersion = '1.0-groovy-2.4' // external dependencies groovy = "org.codehaus.groovy:groovy-all:${groovyVersion}" jacksonDatabind = "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - slf4jApi = "org.slf4j:slf4j-api:${slf4jVersion}" - slf4jSimple = "org.slf4j:slf4j-simple:${slf4jVersion}" spockCore = "org.spockframework:spock-core:${spockVersion}" } @@ -22,12 +19,11 @@ buildscript { task wrapper(type: Wrapper) { - gradleVersion = gradleVersion + gradleVersion = this.gradleVersion } group = 'com.github.jonpeterson' -version = '1.0.0-SNAPSHOT' apply plugin: 'java' apply plugin: 'groovy' @@ -41,9 +37,7 @@ repositories { dependencies { compile jacksonDatabind - compile slf4jApi testCompile groovy - testCompile slf4jSimple testCompile spockCore } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2ba6280 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=1.0.0-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f35f64..4a690d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Jul 28 10:56:21 EDT 2016 +#Sun Jul 31 02:03:03 EDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/gradlew b/gradlew index 9d82f78..27309d9 100644 --- a/gradlew +++ b/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..832fdb6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java new file mode 100644 index 0000000..7a7bf0b --- /dev/null +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/JsonVersionedModel.java @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016 Jon Peterson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.jonpeterson.jackson.module.versioning; + +import com.fasterxml.jackson.annotation.JacksonAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies converter for used in resolving versioning for pre-deserialized JSON. + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonVersionedModel { + + /** + * @return the current version of the model; this will be added to the serialized model's {@link #propertyName()}. + */ + String currentVersion(); + + /** + * @return class of the converter to use when resolving versioning. + */ + Class converterClass(); + + /** + * @return whether to always send JSON data to converter, even when the data is the same version as the + * {@link #currentVersion()}. + */ + boolean alwaysConvert() default false; + + /** + * @return name of property in which the model's version is stored in JSON. + */ + String propertyName() default "modelVersion"; +} diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModel.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModel.java deleted file mode 100644 index e5d36a0..0000000 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModel.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.jonpeterson.jackson.module.versioning; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public interface VersionedModel { - String MODEL_VERSION_PROPERTY = "modelVersion"; - - @JsonProperty(MODEL_VERSION_PROPERTY) - V getCurrentModelVersion(); -} diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java new file mode 100644 index 0000000..2efaab2 --- /dev/null +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelConverter.java @@ -0,0 +1,17 @@ +package com.github.jonpeterson.jackson.module.versioning; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Converter used by {@link JsonVersionedModel} for resolving model versioning. + */ +public interface VersionedModelConverter { + + /** + * Updates JSON data before deserialization to model. + * + * @param modelVersion version of data + * @param modelData JSON data to update + */ + void convert(String modelVersion, ObjectNode modelData); +} diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDataTransformer.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDataTransformer.java deleted file mode 100644 index 4b64172..0000000 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDataTransformer.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.jonpeterson.jackson.module.versioning; - -import com.fasterxml.jackson.databind.node.ObjectNode; - -public abstract class VersionedModelDataTransformer { - public final V targetVersion; - - public VersionedModelDataTransformer(V targetVersion) { - this.targetVersion = targetVersion; - } - - abstract void transform(V modelVersion, ObjectNode modelData); -} diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java index 7cbbd9f..696f78f 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelDeserializer.java @@ -1,59 +1,62 @@ package com.github.jonpeterson.jackson.module.versioning; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TreeTraversingParser; import java.io.IOException; -import java.lang.reflect.ParameterizedType; -public class VersionedModelDeserializer extends StdDeserializer implements ResolvableDeserializer { - private final JsonDeserializer delegateDeserializer; - private final VersionedModelDataTransformer transformer; +class VersionedModelDeserializer extends StdDeserializer implements ResolvableDeserializer { + private final StdDeserializer delegate; + private final JsonVersionedModel jsonVersionedModel; + private final VersionedModelConverter converter; - protected VersionedModelDeserializer(JsonDeserializer delegateDeserializer, VersionedModelDataTransformer transformer) { - super(VersionedModel.class); + VersionedModelDeserializer(StdDeserializer delegate, JsonVersionedModel jsonVersionedModel) { + super(delegate.getValueType()); - this.delegateDeserializer = delegateDeserializer; - this.transformer = transformer; + this.delegate = delegate; + this.jsonVersionedModel = jsonVersionedModel; + + try { + this.converter = jsonVersionedModel.converterClass().newInstance(); + } catch(Exception e) { + throw new RuntimeException("unable to create instance of converter '" + jsonVersionedModel.converterClass().getName() + "'", e); + } } @Override public void resolve(DeserializationContext context) throws JsonMappingException { - ((ResolvableDeserializer)delegateDeserializer).resolve(context); + if(delegate instanceof ResolvableDeserializer) + ((ResolvableDeserializer)delegate).resolve(context); } @Override - public VersionedModel deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException { - TreeNode treeNode = parser.readValueAsTree(); + public T deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonNode jsonNode = parser.readValueAsTree(); - if(!(treeNode instanceof ObjectNode)) + if(!(jsonNode instanceof ObjectNode)) throw context.mappingException("value must be a JSON object"); - ObjectNode objectNode = (ObjectNode)treeNode; + ObjectNode modelData = (ObjectNode)jsonNode; - JsonNode modelVersionNode = objectNode.remove(VersionedModel.MODEL_VERSION_PROPERTY); + JsonNode modelVersionNode = modelData.remove(jsonVersionedModel.propertyName()); if(modelVersionNode == null) - throw context.mappingException("'" + VersionedModel.MODEL_VERSION_PROPERTY + "' property was not present"); - - // DEBUG HERE - //Class abc = delegateDeserializer.handledType(); - //((ParameterizedType)abc.getGenericInterfaces()[0]).getActualTypeArguments()[0] + throw context.mappingException("'" + jsonVersionedModel.propertyName() + "' property was not present"); String modelVersion = modelVersionNode.asText(); if(modelVersion == null) - throw context.mappingException("'" + VersionedModel.MODEL_VERSION_PROPERTY + "' property was null"); + throw context.mappingException("'" + jsonVersionedModel.propertyName() + "' property was null"); - if(transformer != null && !modelVersion.equals(transformer.targetVersion)) - transformer.transform(modelVersion, objectNode); + if(jsonVersionedModel.alwaysConvert() || !modelVersion.equals(jsonVersionedModel.currentVersion())) + converter.convert(modelVersion, modelData); - JsonParser objectNodeParser = new TreeTraversingParser(objectNode); - objectNodeParser.nextToken(); - return delegateDeserializer.deserialize(objectNodeParser, context); + JsonParser postInterceptionParser = new TreeTraversingParser(jsonNode, parser.getCodec()); + postInterceptionParser.nextToken(); + return delegate.deserialize(postInterceptionParser, context); } -} +} \ No newline at end of file diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java new file mode 100644 index 0000000..c971340 --- /dev/null +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersionedModelSerializer.java @@ -0,0 +1,51 @@ +package com.github.jonpeterson.jackson.module.versioning; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.ser.ResolvableSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +class VersionedModelSerializer extends StdSerializer implements ResolvableSerializer { + private final StdSerializer delegate; + private final JsonVersionedModel jsonVersionedModel; + + VersionedModelSerializer(StdSerializer delegate, JsonVersionedModel jsonVersionedModel) { + super(delegate.handledType()); + + this.delegate = delegate; + this.jsonVersionedModel = jsonVersionedModel; + } + + @Override + public void resolve(SerializerProvider provider) throws JsonMappingException { + if(delegate instanceof ResolvableSerializer) + ((ResolvableSerializer)delegate).resolve(provider); + } + + @Override + public void serialize(T value, JsonGenerator generator, SerializerProvider provider) throws IOException { + // serialize the value into a byte array buffer then parse it back out into a JsonNode tree + // TODO: find a better way to convert the value into a tree + JsonFactory factory = generator.getCodec().getFactory(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(4096); + JsonGenerator bufferGenerator = factory.createGenerator(buffer); + try { + delegate.serialize(value, bufferGenerator, provider); + } finally { + bufferGenerator.close(); + } + ObjectNode node = factory.createParser(buffer.toByteArray()).readValueAsTree(); + + // add current version + node.put(jsonVersionedModel.propertyName(), jsonVersionedModel.currentVersion()); + + // write node + generator.writeTree(node); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java index 7329d46..881252e 100644 --- a/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java +++ b/src/main/java/com/github/jonpeterson/jackson/module/versioning/VersioningModule.java @@ -1,37 +1,55 @@ package com.github.jonpeterson.jackson.module.versioning; -import com.fasterxml.jackson.databind.BeanDescription; -import com.fasterxml.jackson.databind.DeserializationConfig; -import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.util.HashMap; -import java.util.Map; - +/** + * Jackson module to load when using {@link JsonVersionedModel}. + */ public class VersioningModule extends SimpleModule { - private final Map, VersionedModelDataTransformer> transformers = new HashMap, VersionedModelDataTransformer>(); - @SuppressWarnings("unchecked") - VersioningModule() { + public VersioningModule() { super("VersioningModule"); setDeserializerModifier(new BeanDeserializerModifier() { @Override public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDescription, JsonDeserializer deserializer) { - if(VersionedModel.class.isAssignableFrom(beanDescription.getBeanClass())) - return new VersionedModelDeserializer( - (JsonDeserializer)deserializer, - transformers.get(beanDescription.getBeanClass()) - ); + if(deserializer instanceof StdDeserializer) { + JsonVersionedModel jsonVersionedModel = beanDescription.getClassAnnotations().get(JsonVersionedModel.class); + if(jsonVersionedModel != null) + return createVersioningDeserializer((StdDeserializer)deserializer, jsonVersionedModel); + } return deserializer; } + + // here just to make generics work without warnings + private VersionedModelDeserializer createVersioningDeserializer(StdDeserializer deserializer, JsonVersionedModel jsonVersionedModel) { + return new VersionedModelDeserializer(deserializer, jsonVersionedModel); + } }); - } - public VersioningModule withTransformer(Class> modelClass, VersionedModelDataTransformer transformer) { - transformers.put(modelClass, transformer); - return this; + setSerializerModifier(new BeanSerializerModifier() { + + @Override + public JsonSerializer modifySerializer(SerializationConfig config, BeanDescription beanDescription, JsonSerializer serializer) { + if(serializer instanceof StdSerializer) { + JsonVersionedModel jsonVersionedModel = beanDescription.getClassAnnotations().get(JsonVersionedModel.class); + if(jsonVersionedModel != null) + return createVersioningSerializer((StdSerializer)serializer, jsonVersionedModel); + } + + return serializer; + } + + // here just to make generics work without warnings + private VersionedModelSerializer createVersioningSerializer(StdSerializer serializer, JsonVersionedModel jsonVersionedModel) { + return new VersionedModelSerializer(serializer, jsonVersionedModel); + } + }); } } diff --git a/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy b/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy index c592e4d..3c18d33 100644 --- a/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy +++ b/src/test/groovy/com/github/jonpeterson/jackson/module/versioning/VersioningModuleTest.groovy @@ -2,52 +2,183 @@ package com.github.jonpeterson.jackson.module.versioning import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.node.ObjectNode import spock.lang.Specification class VersioningModuleTest extends Specification { - static class SomeModel implements VersionedModel { - static final CURRENT_MODEL_VERSION = '1.2' + static class CarsByType { + String type + List cars + List customVersionedCars + } + + @JsonVersionedModel(currentVersion = '3', converterClass = CarVersionedModelConverter) + static class Car { + String make + String model + int year + boolean used + String originalModelVersion + } + + @JsonVersionedModel(currentVersion = '3', converterClass = CarVersionedModelConverter, alwaysConvert = true, propertyName = '_version') + static class CustomVersionedCar extends Car { + } - String fieldA - int fieldB + static class CarVersionedModelConverter implements VersionedModelConverter { @Override - String getCurrentModelVersion() { - return CURRENT_MODEL_VERSION + def void convert(String modelVersion, ObjectNode modelData) { + // model version is an int + def modelVersionNum = modelVersion as int + + // version 1 had a single 'model' field that combined 'make' and 'model' with a colon delimiter; split + if(modelVersionNum < 2) { + def makeAndModel = modelData.get('model').asText().split(':') + modelData.put('make', makeAndModel[0]) + modelData.put('model', makeAndModel[1]) + } + + // version 1-2 had a 'new' text field instead of a boolean 'used' field; convert and invert + if(modelVersionNum < 3) + modelData.put('used', !(modelData.remove('new').asText() as boolean)) + + // setting a debug field + modelData.put('originalModelVersion', modelVersion) } } - def 'abc'() { + + def 'deserialize and reserialize'() { given: def mapper = new ObjectMapper() .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .registerModule(new VersioningModule() - .withTransformer(SomeModel, new VersionedModelDataTransformer(SomeModel.CURRENT_MODEL_VERSION) { - @Override - def void transform(String modelVersion, ObjectNode modelData) { - switch(modelVersion) { - case '1.0': - modelData.put('fieldB', modelData.get('fieldB').asInt() / 2) - break - - case '1.1': - modelData.put('fieldA', modelData.remove('fieldZ').asText().reverse()) - break - } - } - }) - ) + .enable(SerializationFeature.INDENT_OUTPUT) + .registerModule(new VersioningModule()) expect: - mapper.writeValueAsString(mapper.readValue(inputJson, SomeModel)) == expectedOutputJson - - where: - inputJson | expectedOutputJson - '{"fieldA":"abc","fieldB":246,"modelVersion":"1.0"}' | '{"fieldA":"abc","fieldB":123,"modelVersion":"1.2"}' - '{"fieldZ":"cba","fieldB":123,"modelVersion":"1.1"}' | '{"fieldA":"abc","fieldB":123,"modelVersion":"1.2"}' - '{"fieldA":"abc","fieldB":123,"modelVersion":"1.2"}' | '{"fieldA":"abc","fieldB":123,"modelVersion":"1.2"}' - '{"fieldA":"abc","fieldB":123,"modelVersion":1.3}' | '{"fieldA":"abc","fieldB":123,"modelVersion":"1.2"}' + def serialized = mapper.readValue( + '''{ + | "type": "sedan", + | "cars": [ + | { + | "model": "honda:civic", + | "year": 2016, + | "new": "true", + | "modelVersion": "1" + | }, { + | "make": "toyota", + | "model": "camry", + | "year": 2012, + | "new": "false", + | "modelVersion": "2" + | }, { + | "make": "mazda", + | "model": "6", + | "year": 2017, + | "used": false, + | "modelVersion": "3" + | }, { + | "make": "ford", + | "model": "fusion", + | "year": 2013, + | "used": true, + | "modelVersion": "4" + | } + | ], + | "customVersionedCars": [ + | { + | "model": "honda:civic", + | "year": 2016, + | "new": "true", + | "_version": "1" + | }, { + | "make": "toyota", + | "model": "camry", + | "year": 2012, + | "new": "false", + | "_version": "2" + | }, { + | "make": "mazda", + | "model": "6", + | "year": 2017, + | "used": false, + | "_version": "3" + | }, { + | "make": "ford", + | "model": "fusion", + | "year": 2013, + | "used": true, + | "_version": "4" + | } + | ] + |}'''.stripMargin(), + CarsByType + ) + + mapper.writeValueAsString(serialized).replaceAll('\r\n', '\n') == + '''{ + | "cars" : [ { + | "make" : "honda", + | "model" : "civic", + | "originalModelVersion" : "1", + | "used" : false, + | "year" : 2016, + | "modelVersion" : "3" + | }, { + | "make" : "toyota", + | "model" : "camry", + | "originalModelVersion" : "2", + | "used" : false, + | "year" : 2012, + | "modelVersion" : "3" + | }, { + | "make" : "mazda", + | "model" : "6", + | "originalModelVersion" : null, + | "used" : false, + | "year" : 2017, + | "modelVersion" : "3" + | }, { + | "make" : "ford", + | "model" : "fusion", + | "originalModelVersion" : "4", + | "used" : true, + | "year" : 2013, + | "modelVersion" : "3" + | } ], + | "customVersionedCars" : [ { + | "make" : "honda", + | "model" : "civic", + | "originalModelVersion" : "1", + | "used" : false, + | "year" : 2016, + | "_version" : "3" + | }, { + | "make" : "toyota", + | "model" : "camry", + | "originalModelVersion" : "2", + | "used" : false, + | "year" : 2012, + | "_version" : "3" + | }, { + | "make" : "mazda", + | "model" : "6", + | "originalModelVersion" : "3", + | "used" : false, + | "year" : 2017, + | "_version" : "3" + | }, { + | "make" : "ford", + | "model" : "fusion", + | "originalModelVersion" : "4", + | "used" : true, + | "year" : 2013, + | "_version" : "3" + | } ], + | "type" : "sedan" + |}'''.stripMargin() } -} +} \ No newline at end of file