From b6f0e0489ea742314d41ef9ae88dcb8110caf9c5 Mon Sep 17 00:00:00 2001 From: "milo.simpson" Date: Tue, 24 Dec 2013 23:51:13 -0600 Subject: [PATCH 1/2] Cleanup and Refactor of JsonUtils - New JsonUtil interface "extracted" from existing JsonUtils class -- Interface and Impl will allow clients to specify their own preconfigured ObjectMapper - JsonUtils static methods refactored to use a "stock" JsonUtil instance. - Two JsonUtils methods deprecated - Unit test of new JsonUtilImpl ability to take in a preconfigured Jackson Module -- Unit test morphed into a fancy Jackson processing --- Built and tested 5 versions ;) -- Sucessfully implemented recursive polymorphic JSON deserialization in Jackson 2.2 - Bumped Jackson, Guava, TestNg, ArgParse versions. Fixed bug in JsonUtils - The problem was that JsonUtils static methods were using Object.class.getResourceAsStream() which can behave oddly. - Now that the static methods in JsonUtils are backed by a real instance of JsonUtil, that JsonUtil can use itself to load resources. Updated Github Readme docs. - sample code - linked to demo --- README.md | 67 ++--- .../bazaarvoice/jolt/DiffyCliProcessor.java | 2 +- .../bazaarvoice/jolt/JoltCliUtilities.java | 4 +- .../com/bazarvoice/jolt/ChainrFactory.java | 2 +- .../bazaarvoice/jolt/ChainrFactoryTest.java | 8 +- gettingStarted.md | 127 +++++----- .../bazaarvoice/jolt/sample/JoltSample.java | 36 +++ .../src/test/resources/json/sample/input.json | 10 + .../src/test/resources/json/sample/spec.json | 27 ++ .../java/com/bazaarvoice/jolt/JsonUtil.java | 74 ++++++ .../com/bazaarvoice/jolt/JsonUtilImpl.java | 238 ++++++++++++++++++ .../java/com/bazaarvoice/jolt/JsonUtils.java | 180 ++++++------- .../com/bazaarvoice/jolt/JsonUtilsTest.java | 28 ++- .../jolt/TestInstanceOfVSEnumSwitch.java | 210 ++++++++++++++++ .../jolt/jsonUtil/testdomain/QueryFilter.java | 31 +++ .../jolt/jsonUtil/testdomain/QueryParam.java | 31 +++ .../jolt/jsonUtil/testdomain/Readme.md | 58 +++++ .../jolt/jsonUtil/testdomain/RealFilter.java | 66 +++++ .../testdomain/five/BooleanRealFilter5.java | 44 ++++ .../testdomain/five/DateRealFilter5.java | 44 ++++ .../jolt/jsonUtil/testdomain/five/Field.java | 28 +++ .../testdomain/five/IntegerRealFilter5.java | 44 ++++ .../testdomain/five/LogicalFilter5.java | 89 +++++++ .../testdomain/five/MappingTest5.java | 138 ++++++++++ .../jsonUtil/testdomain/five/Operator.java | 30 +++ .../testdomain/five/QueryFilter5.java | 25 ++ .../jsonUtil/testdomain/five/RealFilter5.java | 64 +++++ .../testdomain/five/StringRealFilter5.java | 45 ++++ .../testdomain/four/BaseRealFilter4.java | 61 +++++ .../testdomain/four/BooleanRealFilter4.java | 37 +++ .../testdomain/four/IntegerRealFilter4.java | 37 +++ .../testdomain/four/LogicalFilter4.java | 105 ++++++++ .../testdomain/four/MappingTest4.java | 138 ++++++++++ .../testdomain/four/QueryFilter4.java | 31 +++ .../testdomain/four/StringRealFilter4.java | 37 +++ .../testdomain/one/LogicalFilter1.java | 65 +++++ .../jsonUtil/testdomain/one/MappingTest1.java | 130 ++++++++++ .../testdomain/three/LogicalFilter3.java | 115 +++++++++ .../testdomain/three/MappingTest3.java | 128 ++++++++++ .../testdomain/two/LogicalFilter2.java | 70 ++++++ .../jsonUtil/testdomain/two/MappingTest2.java | 160 ++++++++++++ .../jsonUtils/queryFilter-realOnly.json | 10 + .../five/queryFilter-realAndLogical5.json | 66 +++++ .../four/queryFilter-realAndLogical4.json | 37 +++ .../one/queryFilter-realAndLogical.json | 40 +++ .../two/queryFilter-realAndLogical2.json | 39 +++ parent/pom.xml | 8 +- 47 files changed, 2861 insertions(+), 203 deletions(-) create mode 100644 jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java create mode 100644 jolt-core/src/test/resources/json/sample/input.json create mode 100644 jolt-core/src/test/resources/json/sample/spec.json create mode 100644 json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java create mode 100644 json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java create mode 100644 json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java create mode 100644 json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json create mode 100644 json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json create mode 100644 json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json create mode 100644 json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json create mode 100644 json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json diff --git a/README.md b/README.md index 0a9894b4..7ffe862f 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ JSON to JSON transformation library written in Java where the "specification" fo * [1 Overview](#Overview) * [2 Documentation](#Documentation) * [3 Shiftr Transform DSL](#Shiftr_Transform_DSL) - * [4 Getting Started](#Getting_Started) - * [5 Getting Transform Help](#Getting_Transform_Help) - * [6 Alternatives](#Alternatives) - * [7 Performance](#Performance) - * [8 CLI](#CLI) - * [9 Code Coverage](#Code_Coverage) - * [10 Release Notes](#Release_Notes) + * [4 Demo](#Demo) + * [5 Getting Started](#Getting_Started) + * [6 Getting Transform Help](#Getting_Transform_Help) + * [7 Alternatives](#Alternatives) + * [8 Performance](#Performance) + * [9 CLI](#CLI) + * [10 Code Coverage](#Code_Coverage) + * [11 Release Notes](#Release_Notes) ## Overview @@ -60,15 +61,14 @@ Running a Jolt transform means creating an instance of [Chainr](https://github.c The JSON spec for Chainr looks like : [unit test](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/test/resources/json/chainr/firstSample.json). The Java side looks like : -``` -Chainr chainr = new Chainr( ...getResourceAsStream( "/path/to/chainr/spec.json" ) ); -Object input = elasticSearchHit.getSource(); // ElasticSearch already returns hydrated JSon + Chainr chainr = JsonUtils.classpathToList( "/path/to/chainr/spec.json" ); -Object output = chainr.transform( input ); + Object input = elasticSearchHit.getSource(); // ElasticSearch already returns hydrated JSon -return output; -``` + Object output = chainr.transform( input ); + + return output; ### Shiftr Transform DSL @@ -76,21 +76,20 @@ The Shiftr transform generally does most of the "heavy lifting" in the transform To see the Shiftr DSL in action, please look at our unit tests ([shiftr tests](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/shiftr)) for nice bite sized transform examples, and read the Shiftr [docs](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java). Our unit tests follow the pattern : -``` -{ - "input": { - // sample input - }, - - "spec": { - // transform spec - }, - - "expected": { - // what the output of the transform looks like + + { + "input": { + // sample input + }, + + "spec": { + // transform spec + }, + + "expected": { + // what the output of the transform looks like + } } -} -``` We read in "input", apply the "spec", and [Diffy](https://github.com/bazaarvoice/jolt/blob/master/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java) it against the "expected". @@ -99,9 +98,19 @@ To learn the Shiftr DSL, examine "input" and "output" json, get an understanding For reference, [this](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/test/resources/json/shiftr/firstSample.json) was the very first test we wrote. +## Demo + +There is a demo available at [jolt-demo.appspot.com](http://jolt-demo.appspot.com/). +You can paste in JSON input data and a Spec, and it will post the data to server and run the transform. + +Note + +* it is hosted on a free Google App Engine instance, so it may take a minute to spin up. +* it validates in input JSON and spec client side, but if there are any errors server side it just silently fails. + ## Getting Started -Has it's [own doc](gettingStarted.md). +Getting started code wise has it's [own doc](gettingStarted.md). ## Getting Transform Help @@ -158,4 +167,4 @@ Currently code coverage is at 89% line, and 81% branch. ## Release Notes -On the [Github Jolt Wiki](https://github.com/bazaarvoice/jolt/wiki/Release-Notes). +[Versions and Release Notes available here](https://github.com/bazaarvoice/jolt/releases). diff --git a/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java b/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java index f75c6dbf..c57642bc 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java +++ b/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java @@ -88,7 +88,7 @@ public boolean process( Namespace ns ) { JsonUtils.toPrettyJsonString( result.expected ) + "\n" + "Input #2 contained this:\n" + JsonUtils.toPrettyJsonString( result.actual ), suppressOutput ); - } catch ( IOException e ) { + } catch ( Exception e ) { JoltCliUtilities.printToStandardOut( "Differences found, but diffy encountered an error while writing the result.", suppressOutput ); } finally { return false; diff --git a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java b/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java index 70381fbc..6632f9b5 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java +++ b/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java @@ -78,7 +78,7 @@ public static boolean printJsonObject( Object output, Boolean uglyPrint, boolean } else { printToStandardOut( JsonUtils.toPrettyJsonString( output ), suppressOutput ); } - } catch ( IOException e ) { + } catch ( Exception e ) { printToStandardOut( "An error occured while attempting to print the output.", suppressOutput ); return false; } @@ -98,7 +98,7 @@ public static Object readJsonInput( File file, boolean suppressOutput ) { if ( file == null ) { try { jsonObject = JsonUtils.jsonToMap( System.in ); - } catch ( IOException e ) { + } catch ( Exception e ) { if ( e instanceof JsonParseException ) { printToStandardOut( "Standard input did not contain properly formatted JSON.", suppressOutput ); } else { diff --git a/complete/src/main/java/com/bazarvoice/jolt/ChainrFactory.java b/complete/src/main/java/com/bazarvoice/jolt/ChainrFactory.java index e395f3bf..6f668566 100644 --- a/complete/src/main/java/com/bazarvoice/jolt/ChainrFactory.java +++ b/complete/src/main/java/com/bazarvoice/jolt/ChainrFactory.java @@ -94,7 +94,7 @@ public static Chainr fromFile( File chainrSpecFile, ChainrInstantiator chainrIns try { FileInputStream fileInputStream = new FileInputStream( chainrSpecFile ); chainrSpec = JsonUtils.jsonToObject( fileInputStream ); - } catch ( IOException e ) { + } catch ( Exception e ) { throw new RuntimeException( "Unable to load chainr spec file " + chainrSpecFile.getAbsolutePath() ); } return getChainr( chainrInstantiator, chainrSpec ); diff --git a/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java b/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java index 9947b479..8f92fb7f 100644 --- a/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java +++ b/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java @@ -47,7 +47,7 @@ public void testGetChainrInstanceFromClassPath_success() AssertJUnit.assertTrue( "ChainrFactory did not return an instance of Chainr.", result instanceof Chainr ); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load json file.*" ) + @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*" ) public void testGetChainrInstanceFromClassPath_error() throws Exception { ChainrFactory.fromClassPath( CLASSPATH + MALFORMED_INPUT_FILENAME ); @@ -60,7 +60,7 @@ public void testGetChainrInstanceFromClassPathWithInstantiator_success() AssertJUnit.assertTrue( "ChainrFactory did not return an instance of Chainr.", result instanceof Chainr ); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load json file.*" ) + @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*" ) public void testGetChainrInstanceFromClassPathWithInstantiator_error() throws Exception { ChainrFactory.fromClassPath( CLASSPATH + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); @@ -73,7 +73,7 @@ public void testGetChainrInstanceFromFileSystem_success() AssertJUnit.assertTrue( "ChainrFactory did not return an instance of Chainr.", result instanceof Chainr ); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load json file.*" ) + @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*" ) public void testGetChainrInstanceFromFileSystem_error() throws Exception { ChainrFactory.fromFileSystem( fileSystemPath + MALFORMED_INPUT_FILENAME ); @@ -86,7 +86,7 @@ public void testGetChainrInstanceFromFileSystemWithInstantiator_success() AssertJUnit.assertTrue( "ChainrFactory did not return an instance of Chainr.", result instanceof Chainr ); } - @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load json file.*") + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*") public void testGetChainrInstanceFromFileSystemWithInstantiator_error() throws Exception { ChainrFactory.fromFileSystem( fileSystemPath + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); diff --git a/gettingStarted.md b/gettingStarted.md index 84721ec3..0c314741 100644 --- a/gettingStarted.md +++ b/gettingStarted.md @@ -16,7 +16,7 @@ Maven Dependency to Add to your pom file ``` -Where `latest.jolt.version` looks like `0.0.5`, and can be found by looking at the [project's tags](https://github.com/bazaarvoice/jolt/tags). +Where `latest.jolt.version` looks like `0.0.11`, and can be found by looking at the [project's releases](https://github.com/bazaarvoice/jolt/releases). The two maven artifacts are: @@ -33,85 +33,88 @@ The two maven artifacts are: 3. Replace the input and spec file with your own ### JoltSample.java -``` -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; -import java.io.IOException; +Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java). -public class JoltSample { + package com.bazaarvoice.jolt.sample; - public static void main(String[] args) throws IOException { + import com.bazaarvoice.jolt.Chainr; + import com.bazaarvoice.jolt.JsonUtils; - Object chainrSpecJSON = JsonUtils.jsonToObject( JoltSample.class.getResourceAsStream( "chainrSpec.json" ) ); - Chainr chainr = new Chainr( chainrSpecJSON ); + import java.io.IOException; + import java.util.List; - Object inputJSON = JsonUtils.jsonToObject( JoltSample.class.getResourceAsStream( "input.json" ) ); + public class JoltSample { - Object transformedOutput = chainr.transform( inputJSON ); - System.out.println( JsonUtils.toJsonString( transformedOutput ) ); - } -} -``` + public static void main(String[] args) throws IOException { -### input.json -``` -{ - "rating": { - "primary": { - "value": 3 - }, - "quality": { - "value": 3 + List chainrSpecJSON = JsonUtils.classpathToList( "/json/sample/spec.json" ); + Chainr chainr = Chainr.fromSpec( chainrSpecJSON ); + + Object inputJSON = JsonUtils.classpathToObject( "/json/sample/input.json" ); + + Object transformedOutput = chainr.transform( inputJSON ); + System.out.println( JsonUtils.toJsonString( transformedOutput ) ); } } -} -``` -### chainrSpec.json -``` -[ +### /json/sample/input.json +Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/sample/input.json). + { - "operation": "shift", - "spec": { - "rating": { - "primary": { - "value": "Rating" - }, - "*": { - "value": "SecondaryRatings.&1.Value", - "$": "SecondaryRatings.&.Id" - } + "rating": { + "primary": { + "values": 3 + }, + "quality": { + "values": 3 } } - }, - { - "operation": "default", - "spec": { - "Range" : 5, - "SecondaryRatings" : { - "*" : { - "Range" : 5 + } + +### /json/sample/spec.json +Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/sample/spec.json). + + [ + { + "operation": "shift", + "spec": { + "rating": { + "primary": { + "values": "Rating" + }, + "*": { + "values": "SecondaryRatings.&1.Value", + "$": "SecondaryRatings.&.Id" + } + } + } + }, + { + "operation": "default", + "spec": { + "Range" : 5, + "SecondaryRatings" : { + "*" : { + "Range" : 5 + } } } } - } -] -``` + ] ### Output -Minus the pretty formatting, looks like : -``` -{ - "Rating": 3, - "Range": 5, - "SecondaryRatings": { - "quality": { - "Id": "quality", - "Value": 3, - "Range": 5 +With pretty formatting, looks like : + + { + "Rating": 3, + "Range": 5, + "SecondaryRatings": { + "quality": { + "Id": "quality", + "Value": 3, + "Range": 5 + } } } -} -``` diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java new file mode 100644 index 00000000..cc3d9024 --- /dev/null +++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.sample; + +import com.bazaarvoice.jolt.Chainr; +import com.bazaarvoice.jolt.JsonUtils; + +import java.io.IOException; +import java.util.List; + +public class JoltSample { + + public static void main(String[] args) throws IOException { + + List chainrSpecJSON = JsonUtils.classpathToList( "/json/sample/spec.json" ); + Chainr chainr = Chainr.fromSpec( chainrSpecJSON ); + + Object inputJSON = JsonUtils.classpathToObject( "/json/sample/input.json" ); + + Object transformedOutput = chainr.transform( inputJSON ); + System.out.println( JsonUtils.toJsonString( transformedOutput ) ); + } +} diff --git a/jolt-core/src/test/resources/json/sample/input.json b/jolt-core/src/test/resources/json/sample/input.json new file mode 100644 index 00000000..8b7b4223 --- /dev/null +++ b/jolt-core/src/test/resources/json/sample/input.json @@ -0,0 +1,10 @@ +{ + "rating": { + "primary": { + "value": 3 + }, + "quality": { + "value": 3 + } + } +} diff --git a/jolt-core/src/test/resources/json/sample/spec.json b/jolt-core/src/test/resources/json/sample/spec.json new file mode 100644 index 00000000..5f19cf4c --- /dev/null +++ b/jolt-core/src/test/resources/json/sample/spec.json @@ -0,0 +1,27 @@ +[ + { + "operation": "shift", + "spec": { + "rating": { + "primary": { + "value": "Rating" + }, + "*": { + "value": "SecondaryRatings.&1.Value", + "$": "SecondaryRatings.&.Id" + } + } + } + }, + { + "operation": "default", + "spec": { + "Range" : 5, + "SecondaryRatings" : { + "*" : { + "Range" : 5 + } + } + } + } +] diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java new file mode 100644 index 00000000..681fb7bb --- /dev/null +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * Utility methods for getting JSON content loaded from + * the filesystem, the classpath, or in memory Strings. + * + * Also has methods to serialize Java object to JSON strings. + * + * Implementations of this interface can specify their own + * Jackson ObjectMapper so that Domain specific Java Objects + * can successfully be serialized and de-serialized. + */ +public interface JsonUtil { + + // DE-SERIALIZATION + Object jsonToObject( String json ); + Object jsonToObject( InputStream in ); + + Map jsonToMap( String json ); + Map jsonToMap( InputStream in ); + + List jsonToList( String json ); + List jsonToList( InputStream in ); + + Object filepathToObject( String filePath ); + Map filepathToMap( String filePath ); + List filepathToList( String filePath ); + + Object classpathToObject( String classPath ); + Map classpathToMap( String classPath ); + List classpathToList( String classPath ); + + /** + * Use the stringToType method instead. + */ + @Deprecated + T jsonTo( String json, TypeReference typeRef ); + + /** + * Use the streamToType method instead. + */ + @Deprecated + T jsonTo( InputStream in, TypeReference typeRef ); + + T stringToType ( String json, TypeReference typeRef ); + T classpathToType( String json, TypeReference typeRef ); + T fileToType ( String json, TypeReference typeRef ); + T streamToType ( InputStream in, TypeReference typeRef ); + + // SERIALIZATION + String toJsonString( Object obj ); + String toPrettyJsonString( Object obj ); +} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java new file mode 100644 index 00000000..18a1060c --- /dev/null +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java @@ -0,0 +1,238 @@ +/* + * Copyright 2013 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of JsonUtil that allows the user to provide a configured + * Jackson ObjectMapper. + * + * All IOExceptions are caught, wrapped with context, and rethrown as RuntimeExceptions. + */ +public class JsonUtilImpl implements JsonUtil { + + // thread safe: http://wiki.fasterxml.com/JacksonFAQThreadSafety + private final ObjectMapper objectMapper; + private final ObjectWriter prettyPrintWriter; + + private static final TypeReference> mapTypeReference = + new TypeReference>() {}; + private static final TypeReference> listTypeReference = + new TypeReference>() {}; + + public static void configureStockJoltObjectMapper( ObjectMapper objectMapper ) { + + // All Json maps should be deserialized into LinkedHashMaps. + SimpleModule stockModule = new SimpleModule("stockJoltMapping", new Version(1, 0, 0, null, null, null)) + .addAbstractTypeMapping( Map.class, LinkedHashMap.class ); + + objectMapper.registerModule(stockModule); + + // allow the mapper to parse JSON with comments in it + objectMapper.configure( JsonParser.Feature.ALLOW_COMMENTS, true); + } + + /** + * By allowing the user to provide an ObjectMapper, it can be configured with + * knowledge of how to marshall and un-marshall your domain objects. + * + * @param objectMapper a configured Jackson ObjectMapper + */ + public JsonUtilImpl( ObjectMapper objectMapper ) { + + this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; + + configureStockJoltObjectMapper( this.objectMapper ); + prettyPrintWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); + } + + public JsonUtilImpl() { + this( new ObjectMapper() ); + } + + // DE-SERIALIZATION + public Object jsonToObject( String json ) { + return jsonToObject( new ByteArrayInputStream( json.getBytes() ) ); + } + + public Object jsonToObject( InputStream in ) { + try { + return objectMapper.readValue( in, Object.class ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON object from InputStream.", e ); + } + } + + public Map jsonToMap( String json ) { + return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); + } + + public Map jsonToMap( InputStream in ) { + try { + return objectMapper.readValue( in, mapTypeReference ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON map from InputStream.", e ); + } + } + + public List jsonToList( String json ) { + return jsonToList( new ByteArrayInputStream( json.getBytes() ) ); + } + + public List jsonToList( InputStream in ) { + try { + return objectMapper.readValue( in, listTypeReference ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON list from InputStream.", e ); + } + } + + + public Object filepathToObject( String filePath ) { + try { + FileInputStream fileInputStream = new FileInputStream( filePath ); + return jsonToObject( fileInputStream ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON file from: " + filePath ); + } + } + + public Map filepathToMap( String filePath ) { + try { + FileInputStream fileInputStream = new FileInputStream( filePath ); + return jsonToMap( fileInputStream ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON file from: " + filePath ); + } + } + + public List filepathToList( String filePath ) { + try { + FileInputStream fileInputStream = new FileInputStream( filePath ); + return jsonToList( fileInputStream ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON file from: " + filePath ); + } + } + + public Object classpathToObject( String classPath ) { + try { + InputStream inputStream = this.getClass().getResourceAsStream( classPath ); + + return jsonToObject( inputStream ); + } + catch ( Exception e ) { + throw new RuntimeException( "Unable to load JSON object from classPath : " + classPath, e ); + } + } + + public Map classpathToMap( String classPath ) { + try { + InputStream inputStream = this.getClass().getResourceAsStream( classPath ); + return jsonToMap( inputStream ); + } + catch ( Exception e ) { + throw new RuntimeException( "Unable to load JSON map from classPath : " + classPath, e ); + } + } + + public List classpathToList( String classPath ) { + try { + InputStream inputStream = this.getClass().getResourceAsStream( classPath ); + return jsonToList( inputStream ); + } + catch ( Exception e ) { + throw new RuntimeException( "Unable to load JSON map from classPath : " + classPath, e ); + } + } + + @Deprecated + public T jsonTo( InputStream in, TypeReference typeRef ) { + return streamToType(in, typeRef); + } + + @Deprecated + public T jsonTo( String json, TypeReference typeRef ) { + return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); + } + + public T stringToType( String json, TypeReference typeRef ) { + return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); + } + + public T classpathToType( String path, TypeReference typeRef ) { + return streamToType( this.getClass().getResourceAsStream( path ), typeRef ); + } + + public T fileToType ( String filePath, TypeReference typeRef ) { + try { + FileInputStream fileInputStream = new FileInputStream( filePath ); + return streamToType( fileInputStream, typeRef ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON file from: " + filePath ); + } + } + + public T streamToType( InputStream in, TypeReference typeRef ) { + try { + return objectMapper.readValue( in, typeRef ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON object from InputStream.", e ); + } + } + + + // SERIALIZATION + public String toJsonString( Object obj ) { + try { + return objectMapper.writeValueAsString( obj ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to serialize object : " + obj, e ); + } + } + + public String toPrettyJsonString( Object obj ) { + try { + return prettyPrintWriter.writeValueAsString( obj ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to serialize object : " + obj, e ); + } + } +} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java index f3f08b29..f0f59a46 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java @@ -15,59 +15,46 @@ */ package com.bazaarvoice.jolt; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +/** + * Static method convenience wrappers for a JsonUtil configured with a minimal ObjectMapper. + * + * The ObjectMapper use is configured to : + * Allow comments in the JSON strings, + * Hydrates all JSON Maps into LinkedHashMaps. + */ public class JsonUtils { - // thread safe: http://wiki.fasterxml.com/JacksonFAQThreadSafety - private static final ObjectMapper OBJECT_MAPPER; - private static final ObjectWriter PRETTY_PRINT_WRITER; + private static final JsonUtil util = new JsonUtilImpl(); - static { - JsonFactory factory = new JsonFactory(); - factory.enable( JsonParser.Feature.ALLOW_COMMENTS ); - OBJECT_MAPPER = new ObjectMapper( factory ); - PRETTY_PRINT_WRITER = OBJECT_MAPPER.writerWithDefaultPrettyPrinter(); + /** + * Construct a JsonUtil with a Jackson ObjectMapper that has been preconfigured with custom + * Modules or Mixins. + */ + public static JsonUtil customJsonUtil( ObjectMapper mapper ) { + return new JsonUtilImpl( mapper ); } - public static Object cloneJson( Object json ) { - // deep copy a map: - if ( json instanceof Map ) { - Map jsonMap = (Map) json; - Map retvalue = new HashMap(); - for ( String key : jsonMap.keySet() ) { - retvalue.put( key, cloneJson( jsonMap.get( key ) ) ); - } - return retvalue; - } - - // deep copy a list - if ( json instanceof List ) { - List jsonList = (List) json; - List retvalue = new ArrayList( jsonList.size() ); - for ( Object sub : jsonList ) { - retvalue.add( cloneJson( sub ) ); - } - return retvalue; - } + /** + * Makes a deep copy of a Map object by converting it to a String and then + * back onto stock JSON objects. + * + * @param obj object tree to copy + * @return deep copy of the incoming obj + */ + public static Object cloneJson( Object obj ) { - // string, number, null doesn't need copying - return json; + String string = util.toJsonString( obj ); + return util.jsonToObject( string ); } /** @@ -91,8 +78,8 @@ public static void removeRecursive( Object json, String keyToRemove ) { } // regardless, recurse down the tree - for ( String subkey : jsonMap.keySet() ) { - Object value = jsonMap.get( subkey ); + for ( String subKey : jsonMap.keySet() ) { + Object value = jsonMap.get( subKey ); removeRecursive( value, keyToRemove ); } } @@ -125,85 +112,80 @@ public static Map javason( String javason ) return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); } - public static Map jsonToMap( String json ) - throws IOException { - return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); + //// All the methods listed below are static passthrus to the JsonUtil interface + public static Object jsonToObject( String json ) { + return util.jsonToObject( json ); } - public static Object jsonToObject( String json ) - throws IOException { - return jsonToObject( new ByteArrayInputStream( json.getBytes() ) ); + public static Object jsonToObject( InputStream in ) { + return util.jsonToObject( in ); } - public static Object jsonToObject( InputStream in ) - throws IOException { - return OBJECT_MAPPER.readValue( in, Object.class ); + public static Map jsonToMap( String json ) { + return util.jsonToMap( json ); } - public static Map jsonToMap( InputStream in ) - throws IOException { - TypeReference> typeRef - = new TypeReference>() { - }; - return OBJECT_MAPPER.readValue( in, typeRef ); + public static Map jsonToMap( InputStream in ) { + return util.jsonToMap( in ); } - public static T jsonTo( InputStream in, TypeReference typeRef ) - throws IOException { - return OBJECT_MAPPER.readValue( in, typeRef ); + public static List jsonToList( String json ) { + return util.jsonToList( json ); } - public static String toJsonString( Object map ) - throws IOException { - return OBJECT_MAPPER.writeValueAsString( map ); + public static List jsonToList( InputStream in ) { + return util.jsonToList( in ); } - public static String toPrettyJsonString( Object map ) - throws IOException { - return PRETTY_PRINT_WRITER.writeValueAsString( map ); + public static Object filepathToObject( String filePath ) { + return util.filepathToObject( filePath ); } - public static Object filepathToObject( String filepath ) { - Object json; - try { - FileInputStream fileInputStream = new FileInputStream( filepath ); - json = jsonToObject( fileInputStream ); - } catch ( IOException e ) { - throw new RuntimeException( "Unable to load json file " + filepath ); - } - return json; + public static Map filepathToMap( String filePath ) { + return util.filepathToMap( filePath ); } - public static Map filepathToMap( String filepath ) { - Map json; - try { - FileInputStream fileInputStream = new FileInputStream( filepath ); - json = jsonToMap( fileInputStream ); - } catch ( IOException e ) { - throw new RuntimeException( "Unable to load json file " + filepath ); - } - return json; + public static List filepathToList( String filePath ) { + return util.filepathToList( filePath ); } - public static Object classpathToObject( String filepath ) { - Object json; - try { - InputStream inputStream = Object.class.getResourceAsStream( filepath ); - json = jsonToObject( inputStream ); - } catch ( IOException e ) { - throw new RuntimeException( "Unable to load json file " + filepath ); - } - return json; + public static Object classpathToObject( String classPath ) { + return util.classpathToObject( classPath ); } - public static Map classpathToMap( String filepath ) { - Map json; - try { - InputStream inputStream = Object.class.getResourceAsStream( filepath ); - json = jsonToMap( inputStream ); - } catch ( IOException e ) { - throw new RuntimeException( "Unable to load json file " + filepath ); - } - return json; + public static Map classpathToMap( String classPath ) { + return util.classpathToMap( classPath ); + } + + public static List classpathToList( String classPath ) { + return util.classpathToList( classPath ); + } + + public static T classpathToType( String classPath, TypeReference typeRef ) { + return util.classpathToType( classPath, typeRef ); + } + + /** + * Use the stringToType method instead. + */ + @Deprecated + public static T jsonTo( String json, TypeReference typeRef ) { + return util.stringToType( json, typeRef ); + } + + /** + * Use the streamToType method instead. + */ + @Deprecated + public static T jsonTo( InputStream in, TypeReference typeRef ) { + return util.streamToType( in, typeRef ); + } + + public static String toJsonString( Object obj ) { + return util.toJsonString( obj ); + } + + public static String toPrettyJsonString( Object obj ) { + return util.toPrettyJsonString( obj ); } } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java b/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java index 5e4995da..7c725234 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java @@ -23,9 +23,12 @@ import org.testng.annotations.Test; import org.testng.collections.Lists; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; public class JsonUtilsTest { @@ -87,7 +90,7 @@ public void testRemoveRecursive(Object json, String key, Object expected) throws public void runFixtureTests() throws IOException { String testFixture = "/jsonUtils/jsonUtils-removeRecursive.json"; - List> tests = (List>) JsonUtils.classpathToObject( testFixture ); + List> tests = (List>) JsonUtils.classpathToObject( testFixture ); for ( Map testUnit : tests ) { @@ -122,4 +125,27 @@ public void correctExceptionWithImmutableMap() throws IOException { JsonUtils.removeRecursive( top, "c" ); } + + @Test + public void validateJacksonClosesInputStreams() { + + final Set closedSet = new HashSet(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( "{ \"a\" : \"b\" }".getBytes() ) { + @Override + public void close() throws IOException { + closedSet.add("closed"); + super.close(); + } + }; + + // Pass our wrapped InputStream to Jackson via JsonUtils. + Map map = JsonUtils.jsonToMap( byteArrayInputStream ); + + // Verify that we in fact loaded some data + AssertJUnit.assertNotNull( map ); + AssertJUnit.assertEquals( 1, map.size() ); + + // Verify that the close method was in fact called on the InputStream + AssertJUnit.assertEquals( 1, closedSet.size() ); + } } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java b/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java new file mode 100644 index 00000000..88d95540 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java @@ -0,0 +1,210 @@ +/* + * Copyright 2013 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Small test to see if it is more efficient to do instanceof checks + * or to have Concrete subclasses have an Enum type. + * + * Answer : + * InstanceOf Test looping 50000000 + * Took : 24156 + * Typed Test looping 50000000 + * Took : 23571 + * + * It doesn't really matter ;) + */ +public class TestInstanceOfVSEnumSwitch { + + enum Type { + STRING, + INTEGER, + BOOLEAN, + DATE, + LOGICAL + } + + interface EnumBaseInterface { + Type getType(); + List getValues(); + } + + class StringEnum implements EnumBaseInterface { + public Type getType() { return Type.STRING; } + public List getValues() { return Arrays.asList("A", "B"); } + } + class IntegerEnum implements EnumBaseInterface { + public Type getType() { return Type.INTEGER; } + public List getValues() { return Arrays.asList(1, 2); } + } + class BooleanEnum implements EnumBaseInterface { + public Type getType() { return Type.BOOLEAN; } + public List getValues() { return Arrays.asList( true, false); } + } + class DateEnum implements EnumBaseInterface { + public Type getType() { return Type.DATE; } + public List getValues() { return Arrays.asList("10", "11"); } + } + class LogicalEnum implements EnumBaseInterface { + public Type getType() { return Type.LOGICAL; } + public List getValues() { return new ArrayList(); } + } + + private static int LOOP_COUNT = 1000 * 1000 * 50; + + //@Test + public void testTyped () { + + System.out.println( "Typed Test looping " + LOOP_COUNT ); + long begin = System.currentTimeMillis(); + for ( int index = 0; index < LOOP_COUNT; index++) { + int typeToMake = index % 5; + + EnumBaseInterface t; + switch( typeToMake ) { + case 0 : + t = new StringEnum(); break; + case 1 : + t = new IntegerEnum(); break; + case 2 : + t = new BooleanEnum(); break; + case 3 : + t = new DateEnum(); break; + case 4 : + t = new LogicalEnum(); break; + default : + throw new RuntimeException("pants"); + } + + switch( t.getType() ) { + case STRING: + StringEnum s = (StringEnum) t; + List sValues = s.getValues(); + Assert.assertEquals( Arrays.asList("A", "B"), sValues ); + break; + case INTEGER: + IntegerEnum i = (IntegerEnum) t; + List iValues = i.getValues(); + Assert.assertEquals( Arrays.asList( 1, 2), iValues ); + break; + case BOOLEAN: + BooleanEnum b = (BooleanEnum) t; + List bValues = b.getValues(); + Assert.assertEquals( Arrays.asList(true, false), bValues ); + break; + case DATE: + DateEnum d = (DateEnum) t; + List dValues = d.getValues(); + Assert.assertEquals( Arrays.asList("10", "11"), dValues ); + break; + case LOGICAL: + LogicalEnum l = (LogicalEnum) t; + List lValues = l.getValues(); + Assert.assertEquals( 0, lValues.size() ); + break; + } + } + + long end = System.currentTimeMillis(); + + System.out.println( "Took : " + ( end - begin ) ); + } + + + interface InstanceOfInterface { + List getValues(); + } + + class StringInstanceOf implements InstanceOfInterface { + public List getValues() { return Arrays.asList("A", "B"); } + } + class IntegerInstanceOf implements InstanceOfInterface { + public List getValues() { return Arrays.asList(1, 2); } + } + class BooleanInstanceOf implements InstanceOfInterface { + public List getValues() { return Arrays.asList( true, false); } + } + class DateInstanceOf implements InstanceOfInterface { + public List getValues() { return Arrays.asList("10", "11"); } + } + class LogicalInstanceOf implements InstanceOfInterface { + public List getValues() { return new ArrayList(); } + } + + + //@Test + public void testInstanceOf () { + + System.out.println( "InstanceOf Test looping " + LOOP_COUNT ); + long begin = System.currentTimeMillis(); + for ( int index = 0; index < LOOP_COUNT; index++) { + int typeToMake = index % 5; + + InstanceOfInterface t; + switch( typeToMake ) { + case 0 : + t = new StringInstanceOf(); break; + case 1 : + t = new IntegerInstanceOf(); break; + case 2 : + t = new BooleanInstanceOf(); break; + case 3 : + t = new DateInstanceOf(); break; + case 4 : + t = new LogicalInstanceOf(); break; + default : + throw new RuntimeException("pants"); + } + + if ( t instanceof StringInstanceOf ) { + StringInstanceOf s = (StringInstanceOf) t; + List sValues = s.getValues(); + Assert.assertEquals( Arrays.asList("A", "B"), sValues ); + } + else if (t instanceof IntegerInstanceOf) { + IntegerInstanceOf i = (IntegerInstanceOf) t; + List iValues = i.getValues(); + Assert.assertEquals(Arrays.asList(1, 2), iValues); + } + else if (t instanceof BooleanInstanceOf) { + BooleanInstanceOf b = (BooleanInstanceOf) t; + List bValues = b.getValues(); + Assert.assertEquals(Arrays.asList(true, false), bValues); + } + else if (t instanceof DateInstanceOf) { + DateInstanceOf d = (DateInstanceOf) t; + List dValues = d.getValues(); + Assert.assertEquals(Arrays.asList("10", "11"), dValues); + } + else if (t instanceof LogicalInstanceOf) { + LogicalInstanceOf l = (LogicalInstanceOf) t; + List lValues = l.getValues(); + Assert.assertEquals(0, lValues.size()); + } + } + + long end = System.currentTimeMillis(); + + System.out.println( "Took : " + ( end - begin ) ); + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java new file mode 100644 index 00000000..b02052ff --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain; + +import java.util.Map; + +public interface QueryFilter { + + Map getFilters(); + + QueryParam getQueryParam(); + + String getValue(); + + boolean isLogical(); + + boolean isReal(); +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java new file mode 100644 index 00000000..b6a09c6a --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain; + +public enum QueryParam { + + // REAL params + ID, + RATING, + ISFEATURED, + PRODUCTID, + HASPHOTOS, + HASVIDEOS, + + // Logical Params + AND, + OR +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md new file mode 100644 index 00000000..ae6b9204 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md @@ -0,0 +1,58 @@ +# What is going on here? + +This started out was a unit test for the ability to create a JsonUtil with a custom configured ObjectMapper, but +morphed into fancy Jackson Serialization and Deserialization experimentation. + +# Fancy Jackson + +The end goal was, I wanted to have Json that looks sort of like ElasticSearch's, + but that could serialize and deserialize into "nice" concrete Java Objects. + +Start by looking at the Json on /resources/jsonUtils/testdomain/*. + +## TestDomain One : Simple Recursive Polymorphic JSON deserialization in Jackson 2.2 + +The Filter classes all have simple constructors and just use basic Jackson features. +As such, the JSON representation is overly wordy. + +In the MappingTest class a JsonDeserializer is defined and registerd as a Module to the Jackson +ObjectMapper. It is the thing that differentiates Real and Logical filters. + +The "secret sause" was to create a "sub" JsonParser with the "codec" of the parent. + + ObjectNode root = jp.readValueAsTree(); + + // Examine the root node and figure out what it is. + + // Build a JsonParser passing in our objectCodec so that the subJsonParser + // knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( jp.getCodec() ); + + // Now have the subParser read the value as the appropriate type + subJsonParser.readValueAs( LogicalFilter1.class ); + OR + subJsonParser.readValueAs( RealFilter.class ); + +## TestDomain Two + +Simplified the Json by adding complexity to the JsonSerializer and JsonDeserializer Module in the MappingTest2 class. + +## TestDomain Three + +Uses the same JSON structure as TestDomain Two, but moves some of the code out of the Module +in MappingTest2 to the LogicalFilter3 class, via class level @JsonSerialize and @JsonDeserialize. + +## TestDomain Four + +In the previous formulations, the "value" of a RealFilter was always a String. Here I make it typed (String, Integer, +Boolean). + +## TestDomain Five + +Make the "value" of a RealFilter be a typed List of values. This allowed for more overlap between the Real and +Logical QueryFilters as they can share a getValues( List list ) interface, which is kinda nice. + +Tried again to avoid needed a custom JacksonModule for the ObjectMapper, but still could not get it to work. + +Was able to remove the custom @JsonSerialize and @JsonDeserialize inner classes from LogicalFilter5 by making +it "extend Map". Works but is kunky and I would not use it in practice. diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java new file mode 100644 index 00000000..d4c3c145 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Simple / Standard Pojo Jackson Annotations + */ +public class RealFilter implements QueryFilter { + + private final QueryParam queryParam; + private final String value; + + @JsonCreator + public RealFilter( @JsonProperty( "queryParam" ) QueryParam queryParam, + @JsonProperty( "value" ) String value ) { + this.queryParam = queryParam; + this.value = value; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + public String getValue() { + return value; + } + + @Override + @JsonIgnore + public Map getFilters() { + return null; + } + + @Override + @JsonIgnore + public boolean isLogical() { + return false; + } + + @Override + @JsonIgnore + public boolean isReal() { + return true; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java new file mode 100644 index 00000000..97869d6b --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class BooleanRealFilter5 extends RealFilter5 { + + private final List values; + + @JsonCreator + public BooleanRealFilter5(@JsonProperty("field") Field field, + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { + super(field, op); + this.values = values; + } + + @Override + public List getValues() { + return values; + } + + @Override + public Type getType() { + return Type.BOOLEAN; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java new file mode 100644 index 00000000..0a4b59cc --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class DateRealFilter5 extends RealFilter5 { + + private final List values; + + @JsonCreator + public DateRealFilter5(@JsonProperty("field") Field field, + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { + super(field, op); + this.values = values; + } + + @Override + public List getValues() { + return values; + } + + @Override + public Type getType() { + return Type.DATE; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java new file mode 100644 index 00000000..b747d16b --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +public enum Field { + + // REAL params + ID, + RATING, + ISFEATURED, + PRODUCTID, + HASPHOTOS, + HASVIDEOS, + SUBMISSION_TIME +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java new file mode 100644 index 00000000..72438a89 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class IntegerRealFilter5 extends RealFilter5 { + + private final List values; + + @JsonCreator + public IntegerRealFilter5(@JsonProperty("field") Field field, + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { + super(field, op); + this.values = values; + } + + @Override + public List getValues() { + return values; + } + + @Override + public Type getType() { + return Type.INTEGER; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java new file mode 100644 index 00000000..f5f9beaa --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * In comparison to the other implementations of LogicalFilter, this one is "simpler" from a Jackson perspective, aka + * no custom Serializer and Deserializer. + * + * However that simplicity comes at the "complexity" of being a Map ( extends LinkedHashMap ). + * A) it feels odd, and + * B) it is less memory efficient in the main line case. Aka, we don't always convert to and from JSON, but now + * we are paying the computing cost of that all the time. + * + * In the end, I think this is an interesting take on the LogicalFilter, but I would use the + * @JsonSerialize and @JsonDeserialize approach in practice. + */ +public class LogicalFilter5 extends LinkedHashMap> implements QueryFilter5 { + + private final Operator operator; + private final List filters; + + /** + * Jackson side constructor. + */ + @JsonCreator + public LogicalFilter5(Map> map ) { + super(2); + if ( map.size() != 1 ) { + throw new IllegalArgumentException( "Map to build a LogicalFilter5 should be size 1. Was " + map.size() ); + } + + Operator op = map.keySet().iterator().next(); + List filters = map.values().iterator().next(); + + if ( filters == null ) { + throw new IllegalArgumentException( "LogicalFilter5 List> was null." ); + } + + this.operator = op; + this.filters = filters; + + // populate the map that we are for Serialization + super.put( operator, filters ); + } + + /** + * Java side constructor. + */ + public LogicalFilter5(Operator operator, List filters) { + super(2); + this.operator = operator; + this.filters = filters; + + // populate the map that we are for Serialization + super.put( operator, filters ); + } + + @JsonIgnore + @Override + public List getValues() { + return filters; + } + + @JsonIgnore + @Override + public Operator getOperator() { + return operator; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java new file mode 100644 index 00000000..b6762cf4 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.bazaarvoice.jolt.Diffy; +import com.bazaarvoice.jolt.JsonUtil; +import com.bazaarvoice.jolt.JsonUtils; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class MappingTest5 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilter5Deserializer extends JsonDeserializer { + + /** + * I tried moving this logic to be an @JsonDeserialize on the QueryFilter5 interface + * but I could not get it to work. + * + * When this logic was moved there (and other logic was messed with), I would either get + * A) Deserializaion error on the List in the LogicalFilter5 or + * B) stack overflow from bad recursion + * + * The problem with the List in the LogicalFilter5, seemed like it + * looked at the type of the first QueryFilter5, and assumed that all the elements in + * the Array/List would be the same time, which totally breaks the goal of mixing + * Real and Logical QueryFilter subclasses. + */ + @Override + public QueryFilter5 deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectNode root = jp.readValueAsTree(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( jp.getCodec() ); + + // Check if it is a "RealFilter" + JsonNode valuesParam = root.get("values"); + + if ( valuesParam == null ) { + return subJsonParser.readValueAs( LogicalFilter5.class ); + } + if ( ! valuesParam.isArray() ) { + throw new RuntimeException( "Expected an Array"); + } + + return subJsonParser.readValueAs( RealFilter5.class ); + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + ObjectMapper mapper = new ObjectMapper(); + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer( QueryFilter5.class, new QueryFilter5Deserializer() ); + + mapper.registerModule(testModule); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); + + String testFixture = "/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter5 queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); + + // Make sure the hydrated QFilter looks right + AssertJUnit.assertTrue( queryFilter instanceof LogicalFilter5); + LogicalFilter5 andFilter = (LogicalFilter5) queryFilter; + AssertJUnit.assertEquals( Operator.AND, andFilter.getOperator() ); + AssertJUnit.assertNotNull(andFilter.getValues()); + AssertJUnit.assertEquals(3, andFilter.getValues().size()); + + // Make sure one of the top level RealFilters looks right + QueryFilter5 productIdFilter = andFilter.getValues().get(1); + AssertJUnit.assertTrue( productIdFilter instanceof StringRealFilter5); + StringRealFilter5 stringRealProductIdFilter = (StringRealFilter5) productIdFilter; + AssertJUnit.assertEquals( Field.PRODUCTID, stringRealProductIdFilter.getField() ); + AssertJUnit.assertEquals( Operator.EQ, stringRealProductIdFilter.getOperator() ); + AssertJUnit.assertEquals( "Acme-1234", stringRealProductIdFilter.getValues().get(0) ); + + // Make sure the nested OR looks right + QueryFilter5 orFilter = andFilter.getValues().get(2); + AssertJUnit.assertTrue( orFilter instanceof LogicalFilter5 ); + LogicalFilter5 realOrFilter = (LogicalFilter5) orFilter; + AssertJUnit.assertEquals( Operator.OR, realOrFilter.getOperator() ); + AssertJUnit.assertEquals( 2, realOrFilter.getValues().size() ); + + // Make sure nested AND looks right + QueryFilter5 nestedAndFilter = realOrFilter.getValues().get(1); + AssertJUnit.assertTrue( nestedAndFilter instanceof LogicalFilter5 ); + AssertJUnit.assertEquals( Operator.AND, nestedAndFilter.getOperator() ); + AssertJUnit.assertEquals( 3, nestedAndFilter.getValues().size() ); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString( queryFilter ); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap( unitTestString ); + Map expected = JsonUtils.classpathToMap( testFixture ); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff( expected, actual ); + if (!result.isEmpty()) { + AssertJUnit.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java new file mode 100644 index 00000000..cacd1fc4 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +public enum Operator { + + AND, + OR, + + EQ, + NEQ, + + GT, + GTE, + LT, + LTE +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java new file mode 100644 index 00000000..8b3271c0 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java @@ -0,0 +1,25 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import java.util.List; + +public interface QueryFilter5 { + + Operator getOperator(); + + List getValues(); +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java new file mode 100644 index 00000000..b30069d4 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.util.List; + +/** + * Tell Jackson to use the "type" field to know which subclass to initialize. + * + * E.g. "type" : "INTEGER" --> Deserialize a IntegerRealFilter5 + */ +@JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.PROPERTY,property="type") +@JsonSubTypes({ + @JsonSubTypes.Type(value=IntegerRealFilter5.class,name="INTEGER"), + @JsonSubTypes.Type(value=StringRealFilter5.class,name="STRING"), + @JsonSubTypes.Type(value=DateRealFilter5.class,name="DATE"), + @JsonSubTypes.Type(value=BooleanRealFilter5.class,name="BOOLEAN")}) +public abstract class RealFilter5 implements QueryFilter5 { + + public enum Type { + STRING, + INTEGER, + BOOLEAN, + DATE + } + + private final Field field; + private final Operator op; + + public RealFilter5(Field field, + Operator op) { + this.field = field; + this.op = op; + } + + public Field getField() { + return field; + } + + @Override + public Operator getOperator() { + return op; + } + + public abstract List getValues(); + + public abstract Type getType(); +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java new file mode 100644 index 00000000..0595dc00 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.five; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class StringRealFilter5 extends RealFilter5 { + + private final List values; + + + @JsonCreator + public StringRealFilter5(@JsonProperty("field") Field field, + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { + super(field, op); + this.values = values; + } + + @Override + public List getValues() { + return values; + } + + @Override + public Type getType() { + return Type.STRING; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java new file mode 100644 index 00000000..70c3a045 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Simple / Standard Pojo Jackson Annotations + */ +public abstract class BaseRealFilter4 implements QueryFilter4 { + + private final QueryParam queryParam; + + @JsonCreator + public BaseRealFilter4(@JsonProperty("queryParam") QueryParam queryParam ) { + this.queryParam = queryParam; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + @JsonIgnore + public Map getFilters() { + return null; + } + + @Override + @JsonIgnore + public boolean isLogical() { + return false; + } + + @Override + @JsonIgnore + public boolean isReal() { + return true; + } + + public abstract T getValue(); +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java new file mode 100644 index 00000000..9a432978 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BooleanRealFilter4 extends BaseRealFilter4 { + + private final Boolean value; + + @JsonCreator + public BooleanRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, + @JsonProperty("value") Boolean value) { + super(queryParam); + this.value = value; + } + + @Override + public Boolean getValue() { + return value; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java new file mode 100644 index 00000000..719a0929 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IntegerRealFilter4 extends BaseRealFilter4 { + + private final Integer value; + + @JsonCreator + public IntegerRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, + @JsonProperty("value") Integer value) { + super(queryParam); + this.value = value; + } + + @Override + public Integer getValue() { + return value; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java new file mode 100644 index 00000000..3da0d351 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java @@ -0,0 +1,105 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +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.node.ObjectNode; + +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@JsonSerialize(using = LogicalFilter4.LogicalFilter4Serializer.class) +@JsonDeserialize(using = LogicalFilter4.LogicalFilter4Deserializer.class) +public class LogicalFilter4 implements QueryFilter4 { + + public static class LogicalFilter4Serializer extends JsonSerializer { + + @Override + public void serialize(LogicalFilter4 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); + jgen.writeEndObject(); + } + } + + public static class LogicalFilter4Deserializer extends JsonDeserializer { + + @Override + public LogicalFilter4 deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException, JsonProcessingException { + + ObjectCodec objectCodec = jp.getCodec(); + ObjectNode root = jp.readValueAsTree(); + + // We assume it is a LogicalFilter + Iterator iter = root.fieldNames(); + String key = iter.next(); + + JsonNode arrayNode = root.iterator().next(); + if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { + throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); + } + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse( objectCodec ); + List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); + + return new LogicalFilter4( QueryParam.valueOf( key ), childrenQueryFilters ); + } + } + + private QueryParam queryParam; + private Map filters; + + public LogicalFilter4(QueryParam queryParam, List filters) { + this.queryParam = queryParam; + + this.filters = new LinkedHashMap(); + for ( QueryFilter4 queryFilter : filters ) { + this.filters.put( queryFilter.getQueryParam(), queryFilter ); + } + } + + @Override + public Map getFilters() { + return filters; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + public boolean isLogical() { + return true; + } + + @Override + public boolean isReal() { + return false; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java new file mode 100644 index 00000000..c4fd61d9 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.Diffy; +import com.bazaarvoice.jolt.JsonUtil; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class MappingTest4 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilter4Deserializer extends JsonDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter4 deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectNode root = jp.readValueAsTree(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( jp.getCodec() ); + + // Check if it is a "RealFilter" + JsonNode valueParam = root.get("value"); + + if ( valueParam == null ) { + return subJsonParser.readValueAs( LogicalFilter4.class ); + } + if ( valueParam.isBoolean() ) { + return subJsonParser.readValueAs( BooleanRealFilter4.class ); + } + else if ( valueParam.isTextual() ) { + return subJsonParser.readValueAs( StringRealFilter4.class ); + } + else if ( valueParam.isIntegralNumber() ) { + return subJsonParser.readValueAs( IntegerRealFilter4.class ); + } + else { + throw new RuntimeException("Unknown type"); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + ObjectMapper mapper = new ObjectMapper(); + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer( QueryFilter4.class, new QueryFilter4Deserializer() ); + + mapper.registerModule(testModule); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); + + String testFixture = "/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter4 queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); + + // Make sure the hydrated QFilter looks right + AssertJUnit.assertTrue( queryFilter instanceof LogicalFilter4); + AssertJUnit.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); + AssertJUnit.assertTrue( queryFilter.isLogical() ); + AssertJUnit.assertEquals( 3, queryFilter.getFilters().size() ); + AssertJUnit.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); + + // Make sure one of the top level RealFilters looks right + QueryFilter4 productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); + AssertJUnit.assertTrue( productIdFilter.isReal() ); + AssertJUnit.assertTrue( productIdFilter instanceof StringRealFilter4); + StringRealFilter4 stringRealProductIdFilter = (StringRealFilter4) productIdFilter; + AssertJUnit.assertEquals( QueryParam.PRODUCTID, stringRealProductIdFilter.getQueryParam() ); + AssertJUnit.assertEquals( "Acme-1234", stringRealProductIdFilter.getValue() ); + + // Make sure the nested OR looks right + QueryFilter4 orFilter = queryFilter.getFilters().get( QueryParam.OR ); + AssertJUnit.assertTrue( orFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, orFilter.getFilters().size() ); + + // Make sure nested AND looks right + QueryFilter4 nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); + AssertJUnit.assertTrue( nestedAndFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, nestedAndFilter.getFilters().size() ); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString( queryFilter ); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap( unitTestString ); + Map expected = JsonUtils.classpathToMap( testFixture ); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff( expected, actual ); + if (!result.isEmpty()) { + AssertJUnit.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java new file mode 100644 index 00000000..755530f5 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; + +import java.util.Map; + +public interface QueryFilter4 { + + Map getFilters(); + + QueryParam getQueryParam(); + + boolean isLogical(); + + boolean isReal(); +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java new file mode 100644 index 00000000..109bec12 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.four; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class StringRealFilter4 extends BaseRealFilter4 { + + private final String value; + + @JsonCreator + public StringRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, + @JsonProperty("value") String value) { + super(queryParam); + this.value = value; + } + + @Override + public String getValue() { + return value; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java new file mode 100644 index 00000000..db4b7d27 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.one; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public class LogicalFilter1 implements QueryFilter { + + private QueryParam queryParam; + private Map filters; + + @JsonCreator + public LogicalFilter1( @JsonProperty( "queryParam" ) QueryParam queryParam, + @JsonProperty( "filters" ) Map filters ) { + this.queryParam = queryParam; + this.filters = filters; + } + + @Override + public Map getFilters() { + return filters; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + @JsonIgnore + public String getValue() { + return null; + } + + @Override + @JsonIgnore + public boolean isLogical() { + return true; + } + + @Override + @JsonIgnore + public boolean isReal() { + return false; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java new file mode 100644 index 00000000..14fc5b21 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.one; + +import com.bazaarvoice.jolt.Diffy; +import com.bazaarvoice.jolt.JsonUtil; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class MappingTest1 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilter1Deserializer extends JsonDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectNode root = jp.readValueAsTree(); + + JsonNode queryParam = root.get("queryParam"); + String value = queryParam.asText(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( jp.getCodec() ); + + // Determine the "type" of filter we are dealing with Real or Logical and specify type + if ( "OR".equals( value ) || "AND".equals( value ) ) { + return subJsonParser.readValueAs( LogicalFilter1.class ); + } + else { + return subJsonParser.readValueAs( RealFilter.class ); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + ObjectMapper mapper = new ObjectMapper(); + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer( QueryFilter.class, new QueryFilter1Deserializer() ); + + mapper.registerModule(testModule); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); + + String testFixture = "/jsonUtils/testdomain/one/queryFilter-realAndLogical.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); + + // Make sure the hydrated queryFilter looks right + AssertJUnit.assertTrue( queryFilter instanceof LogicalFilter1 ); + AssertJUnit.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); + AssertJUnit.assertTrue( queryFilter.isLogical() ); + AssertJUnit.assertEquals( 3, queryFilter.getFilters().size() ); + AssertJUnit.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); + AssertJUnit.assertTrue( productIdFilter.isReal() ); + AssertJUnit.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); + AssertJUnit.assertEquals( "Acme-1234", productIdFilter.getValue() ); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); + AssertJUnit.assertTrue( orFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, orFilter.getFilters().size() ); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); + AssertJUnit.assertTrue( nestedAndFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, nestedAndFilter.getFilters().size() ); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString( queryFilter ); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap( unitTestString ); + Map expected = JsonUtils.classpathToMap( testFixture ); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff( expected, actual ); + if (!result.isEmpty()) { + AssertJUnit.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java new file mode 100644 index 00000000..45c725b8 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.three; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@JsonSerialize(using = LogicalFilter3.LogicalFilter3Serializer.class) +@JsonDeserialize(using = LogicalFilter3.LogicalFilter4Deserializer.class) +public class LogicalFilter3 implements QueryFilter { + + public static class LogicalFilter3Serializer extends JsonSerializer { + + @Override + public void serialize(LogicalFilter3 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); + jgen.writeEndObject(); + } + } + + public static class LogicalFilter4Deserializer extends JsonDeserializer { + + @Override + public LogicalFilter3 deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException, JsonProcessingException { + + ObjectCodec objectCodec = jp.getCodec(); + ObjectNode root = jp.readValueAsTree(); + + // We assume it is a LogicalFilter + Iterator iter = root.fieldNames(); + String key = iter.next(); + + JsonNode arrayNode = root.iterator().next(); + if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { + throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); + } + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse( objectCodec ); + List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); + + return new LogicalFilter3( QueryParam.valueOf( key ), childrenQueryFilters ); + } + } + + private QueryParam queryParam; + private Map filters; + + public LogicalFilter3( QueryParam queryParam, List filters ) { + this.queryParam = queryParam; + + this.filters = new LinkedHashMap(); + for ( QueryFilter queryFilter : filters ) { + this.filters.put( queryFilter.getQueryParam(), queryFilter ); + } + } + + @Override + public Map getFilters() { + return filters; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + public String getValue() { + return null; + } + + @Override + public boolean isLogical() { + return true; + } + + @Override + public boolean isReal() { + return false; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java new file mode 100644 index 00000000..151604f3 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java @@ -0,0 +1,128 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.three; + +import com.bazaarvoice.jolt.Diffy; +import com.bazaarvoice.jolt.JsonUtil; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class MappingTest3 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilterDeserializer extends JsonDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectNode root = jp.readValueAsTree(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( jp.getCodec() ); + + // Check if it is a "RealFilter" + JsonNode queryParam = root.get("queryParam"); + if ( queryParam != null && queryParam.isValueNode() ) { + return subJsonParser.readValueAs( RealFilter.class ); + } + else { + return subJsonParser.readValueAs( LogicalFilter3.class ); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + ObjectMapper mapper = new ObjectMapper(); + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer( QueryFilter.class, new QueryFilterDeserializer() ); + + mapper.registerModule(testModule); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); + + String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); + + // Make sure the hydrated QFilter looks right + AssertJUnit.assertTrue( queryFilter instanceof LogicalFilter3 ); + AssertJUnit.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); + AssertJUnit.assertTrue( queryFilter.isLogical() ); + AssertJUnit.assertEquals( 3, queryFilter.getFilters().size() ); + AssertJUnit.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); + AssertJUnit.assertTrue( productIdFilter.isReal() ); + AssertJUnit.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); + AssertJUnit.assertEquals( "Acme-1234", productIdFilter.getValue() ); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); + AssertJUnit.assertTrue( orFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, orFilter.getFilters().size() ); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); + AssertJUnit.assertTrue( nestedAndFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, nestedAndFilter.getFilters().size() ); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString( queryFilter ); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap( unitTestString ); + Map expected = JsonUtils.classpathToMap( testFixture ); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff( expected, actual ); + if (!result.isEmpty()) { + AssertJUnit.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java new file mode 100644 index 00000000..e1427e63 --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.two; + +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Note this class does not have any Jackson markup as all the work is being done + * in the Jackson Module inside CustomObjectMapperTest2... + * + * This is an improvement over LogicalFilter1 in that we write out an Array but still + * have a Map in memory, for easy filter lookup. + */ +public class LogicalFilter2 implements QueryFilter { + + private QueryParam queryParam; + private Map filters; + + public LogicalFilter2( QueryParam queryParam, List filters ) { + this.queryParam = queryParam; + + this.filters = new LinkedHashMap(); + for ( QueryFilter queryFilter : filters ) { + this.filters.put( queryFilter.getQueryParam(), queryFilter ); + } + } + + @Override + public Map getFilters() { + return filters; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + public String getValue() { + return null; + } + + @Override + public boolean isLogical() { + return true; + } + + @Override + public boolean isReal() { + return false; + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java new file mode 100644 index 00000000..cfa17d0d --- /dev/null +++ b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java @@ -0,0 +1,160 @@ +/* + * Copyright 2014 Bazaarvoice, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.bazaarvoice.jolt.jsonUtil.testdomain.two; + +import com.bazaarvoice.jolt.Diffy; +import com.bazaarvoice.jolt.JsonUtil; +import com.bazaarvoice.jolt.JsonUtils; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; +import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.AssertJUnit; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class MappingTest2 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilterDeserializer extends JsonDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + + ObjectCodec objectCodec = jp.getCodec(); + ObjectNode root = jp.readValueAsTree(); + + // Check if it is a "RealFilter" + JsonNode queryParam = root.get("queryParam"); + if ( queryParam != null && queryParam.isValueNode() ) { + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse( objectCodec ); + + return subJsonParser.readValueAs( RealFilter.class ); + } + + // We assume it is a LogicalFilter + Iterator iter = root.fieldNames(); + String key = iter.next(); + + JsonNode arrayNode = root.iterator().next(); + if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { + throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); + } + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse( objectCodec ); + List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); + + return new LogicalFilter2( QueryParam.valueOf( key ), childrenQueryFilters ); + } + } + + public static class LogicalFilter2Serializer extends JsonSerializer { + + @Override + public void serialize(LogicalFilter2 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); + jgen.writeEndObject(); + } + } + + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + ObjectMapper mapper = new ObjectMapper(); + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer( QueryFilter.class, new QueryFilterDeserializer() ) + .addSerializer( LogicalFilter2.class, new LogicalFilter2Serializer() ); + + mapper.registerModule(testModule); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); + + String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); + + // Make sure the hydrated QFilter looks right + AssertJUnit.assertTrue( queryFilter instanceof LogicalFilter2 ); + AssertJUnit.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); + AssertJUnit.assertTrue( queryFilter.isLogical() ); + AssertJUnit.assertEquals( 3, queryFilter.getFilters().size() ); + AssertJUnit.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); + AssertJUnit.assertTrue( productIdFilter.isReal() ); + AssertJUnit.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); + AssertJUnit.assertEquals( "Acme-1234", productIdFilter.getValue() ); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); + AssertJUnit.assertTrue( orFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, orFilter.getFilters().size() ); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); + AssertJUnit.assertTrue( nestedAndFilter.isLogical() ); + AssertJUnit.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); + AssertJUnit.assertEquals( 2, nestedAndFilter.getFilters().size() ); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString( queryFilter ); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap( unitTestString ); + Map expected = JsonUtils.classpathToMap( testFixture ); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff( expected, actual ); + if (!result.isEmpty()) { + AssertJUnit.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + } + } +} diff --git a/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json b/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json new file mode 100644 index 00000000..23c53ebe --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json @@ -0,0 +1,10 @@ +{ + "RATING" : { + "queryParam" : "RATING", + "value" : "3" + }, + "PRODUCTID" : { + "queryParam" : "PRODUCTID", + "value" : "Acme-1234" + } +} \ No newline at end of file diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json b/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json new file mode 100644 index 00000000..376a190a --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json @@ -0,0 +1,66 @@ + +// Even better format. Values are a list and are typed. +// Now that the RealFilters values are a list, Real and Logicial Fitlers can +// share a getValues( List list ) interface, which is kinda nice. +// +// Note the "type" : "INTEGER" line. It is needed because we cannot infer +// the Filter type from the type of the Values. +// +// That said having the "type" : "INTEGER" line means we don't have to have a custom +// Deserializer, as we can use the @JsonTypeInfo and @JsonSubTypes annotations to do the +// work for us. +// +// @JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.PROPERTY,property="type") +// @JsonSubTypes({ +// @JsonSubTypes.Type(value=IntegerRealFilter5.class,name="INTEGER"), +// ... +// }) +{ + "AND" : [ + { + "type" : "INTEGER", + "field": "RATING", + "operator" : "EQ", + "values": [ 3 ] + }, + { + "type" : "STRING", + "field": "PRODUCTID", + "operator" : "EQ", + "values": [ "Acme-1234" ] + }, + { + "OR" : [ + { + "type" : "STRING", + "field": "ID", + "operator" : "EQ", + "values": [ "789" ] + }, + { + "AND" : [ + { + "type" : "BOOLEAN", + "field": "ISFEATURED", + "operator" : "EQ", + "values": [ true ] + }, + { + "type" : "BOOLEAN", + "field": "HASPHOTOS", + "operator" : "EQ", + "values": [ true ] + }, + { + "type" : "DATE", + "field": "SUBMISSION_TIME", + "operator" : "LTE", + "values": [ "1-1-2014" ] // This is the reason the type Enum is needed + // as DATE and STRING both "encode" to JSON strings + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json b/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json new file mode 100644 index 00000000..d4bfdd89 --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json @@ -0,0 +1,37 @@ + +// Even better format. Values are typed. +// This formulation requires a one to one mapping between the JSON type (String, number, boolean) +// and a RealFilter. In practice this would not work as String and Date filters have +// JSON values that are strings. +{ + "AND" : [ + { + "queryParam": "RATING", + "value": 3 + }, + { + "queryParam": "PRODUCTID", + "value": "Acme-1234" + }, + { + "OR" : [ + { + "queryParam": "ID", + "value": "789" + }, + { + "AND" : [ + { + "queryParam": "ISFEATURED", + "value": true + }, + { + "queryParam": "HASPHOTOS", + "value": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json b/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json new file mode 100644 index 00000000..7413bdf0 --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json @@ -0,0 +1,40 @@ + +// This works, but overly wordy. +// The "queryParam" is needlessly repeated. +// +// Also note all the real filter values are Strings, aka "value": "3". +{ + "queryParam": "AND", + "filters" : { + "RATING": { + "queryParam": "RATING", + "value": "3" + }, + "PRODUCTID": { + "queryParam": "PRODUCTID", + "value": "Acme-1234" + }, + "OR": { + "queryParam": "OR", + "filters" : { + "ID" : { + "queryParam": "ID", + "value": "789" + }, + "AND" : { + "queryParam": "AND", + "filters" : { + "ISFEATURED" : { + "queryParam": "ISFEATURED", + "value": "true" + }, + "HASPHOTOS" : { + "queryParam": "HASPHOTOS", + "value": "true" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json b/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json new file mode 100644 index 00000000..30ceccd8 --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json @@ -0,0 +1,39 @@ + +// This is a much nicer formulation / format. +// Silly "queryParam" is gone, but still have all values being Strings, e.g. "value": "3". +// +// Note this Json file is used by both testdomain two and three Java code. +// Java "three" is nicer than "two", in that more logic was moved out of the MappingTest Module +// into the LogicalFilter3 @JsonDeserializer +{ + "AND" : [ + { + "queryParam": "RATING", + "value": "3" + }, + { + "queryParam": "PRODUCTID", + "value": "Acme-1234" + }, + { + "OR" : [ + { + "queryParam": "ID", + "value": "789" + }, + { + "AND" : [ + { + "queryParam": "ISFEATURED", + "value": "true" + }, + { + "queryParam": "HASPHOTOS", + "value": "true" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index d0c205c4..097d7f6e 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -29,13 +29,13 @@ 1 - 2.2.1 + 2.2.3 3.0 - 0.4.1 + 0.4.2 - 14.0.1 - 6.3 + 15.0 + 6.8.7 From 3e49bd52161836bb9f0bac3e53109c60712f60e1 Mon Sep 17 00:00:00 2001 From: "milo.simpson" Date: Thu, 13 Feb 2014 20:59:45 -0600 Subject: [PATCH 2/2] Additional JsonUtils methods. --- .../main/java/com/bazaarvoice/jolt/Diffy.java | 17 ++++- .../java/com/bazaarvoice/jolt/JsonUtil.java | 25 ++++++-- .../com/bazaarvoice/jolt/JsonUtilImpl.java | 64 +++++++++++++++++-- .../java/com/bazaarvoice/jolt/JsonUtils.java | 57 +++++++++++++---- 4 files changed, 139 insertions(+), 24 deletions(-) diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java b/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java index 35272bf2..594e440e 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java @@ -30,9 +30,22 @@ */ public class Diffy { + private final JsonUtil jsonUtil; + + public Diffy() { + jsonUtil = JsonUtils.getDefaultJsonUtil(); + } + + /** + * Pass in a custom jsonUtil to use for the cloneJson method. + */ + public Diffy( JsonUtil jsonUtil ) { + this.jsonUtil = jsonUtil; + } + public Result diff(Object expected, Object actual) { - Object expectedCopy = JsonUtils.cloneJson( expected ); - Object actualCopy = JsonUtils.cloneJson( actual ); + Object expectedCopy = jsonUtil.cloneJson( expected ); + Object actualCopy = jsonUtil.cloneJson( actual ); return diffHelper( expectedCopy, actualCopy ); } diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java index 681fb7bb..cff0c87c 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java @@ -63,12 +63,29 @@ public interface JsonUtil { @Deprecated T jsonTo( InputStream in, TypeReference typeRef ); - T stringToType ( String json, TypeReference typeRef ); - T classpathToType( String json, TypeReference typeRef ); - T fileToType ( String json, TypeReference typeRef ); + T stringToType (String json, TypeReference typeRef ); + T stringToType (String json, Class aClass ); + + T classpathToType(String classPath, TypeReference typeRef ); + T classpathToType(String classPath, Class aClass ); + + T fileToType (String filePath, TypeReference typeRef ); + T fileToType (String filePath, Class aClass ); + T streamToType ( InputStream in, TypeReference typeRef ); + T streamToType ( InputStream in, Class aClass ); - // SERIALIZATION String toJsonString( Object obj ); String toPrettyJsonString( Object obj ); + + /** + * Makes a deep copy of a Map object by converting it to a String and then + * back onto stock JSON objects. + * + * Leverages Serialization + * + * @param obj object tree to copy + * @return deep copy of the incoming obj + */ + Object cloneJson( Object obj ); } diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java index 18a1060c..0c9513ed 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java @@ -78,10 +78,12 @@ public JsonUtilImpl() { } // DE-SERIALIZATION + @Override public Object jsonToObject( String json ) { return jsonToObject( new ByteArrayInputStream( json.getBytes() ) ); } + @Override public Object jsonToObject( InputStream in ) { try { return objectMapper.readValue( in, Object.class ); @@ -91,10 +93,12 @@ public Object jsonToObject( InputStream in ) { } } + @Override public Map jsonToMap( String json ) { return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); } + @Override public Map jsonToMap( InputStream in ) { try { return objectMapper.readValue( in, mapTypeReference ); @@ -104,10 +108,12 @@ public Map jsonToMap( InputStream in ) { } } + @Override public List jsonToList( String json ) { return jsonToList( new ByteArrayInputStream( json.getBytes() ) ); } + @Override public List jsonToList( InputStream in ) { try { return objectMapper.readValue( in, listTypeReference ); @@ -118,6 +124,7 @@ public List jsonToList( InputStream in ) { } + @Override public Object filepathToObject( String filePath ) { try { FileInputStream fileInputStream = new FileInputStream( filePath ); @@ -128,6 +135,7 @@ public Object filepathToObject( String filePath ) { } } + @Override public Map filepathToMap( String filePath ) { try { FileInputStream fileInputStream = new FileInputStream( filePath ); @@ -138,6 +146,7 @@ public Map filepathToMap( String filePath ) { } } + @Override public List filepathToList( String filePath ) { try { FileInputStream fileInputStream = new FileInputStream( filePath ); @@ -148,6 +157,7 @@ public List filepathToList( String filePath ) { } } + @Override public Object classpathToObject( String classPath ) { try { InputStream inputStream = this.getClass().getResourceAsStream( classPath ); @@ -159,6 +169,7 @@ public Object classpathToObject( String classPath ) { } } + @Override public Map classpathToMap( String classPath ) { try { InputStream inputStream = this.getClass().getResourceAsStream( classPath ); @@ -169,6 +180,7 @@ public Map classpathToMap( String classPath ) { } } + @Override public List classpathToList( String classPath ) { try { InputStream inputStream = this.getClass().getResourceAsStream( classPath ); @@ -180,24 +192,38 @@ public List classpathToList( String classPath ) { } @Deprecated + @Override public T jsonTo( InputStream in, TypeReference typeRef ) { return streamToType(in, typeRef); } @Deprecated + @Override public T jsonTo( String json, TypeReference typeRef ) { return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); } - public T stringToType( String json, TypeReference typeRef ) { + @Override + public T stringToType( String json, TypeReference typeRef ) { return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); } - public T classpathToType( String path, TypeReference typeRef ) { - return streamToType( this.getClass().getResourceAsStream( path ), typeRef ); + @Override + public T stringToType( String json, Class aClass ) { + return streamToType( new ByteArrayInputStream( json.getBytes() ), aClass ); } - public T fileToType ( String filePath, TypeReference typeRef ) { + @Override + public T classpathToType( String classPath, TypeReference typeRef ) { + return streamToType( this.getClass().getResourceAsStream( classPath ), typeRef ); + } + @Override + public T classpathToType( String classPath, Class aClass ) { + return streamToType( this.getClass().getResourceAsStream( classPath ), aClass ); + } + + @Override + public T fileToType( String filePath, TypeReference typeRef ) { try { FileInputStream fileInputStream = new FileInputStream( filePath ); return streamToType( fileInputStream, typeRef ); @@ -207,6 +233,18 @@ public T fileToType ( String filePath, TypeReference typeRef ) { } } + @Override + public T fileToType( String filePath, Class aClass ) { + try { + FileInputStream fileInputStream = new FileInputStream( filePath ); + return streamToType( fileInputStream, aClass ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON file from: " + filePath ); + } + } + + @Override public T streamToType( InputStream in, TypeReference typeRef ) { try { return objectMapper.readValue( in, typeRef ); @@ -216,8 +254,19 @@ public T streamToType( InputStream in, TypeReference typeRef ) { } } + @Override + public T streamToType( InputStream in, Class aClass ) { + try { + return objectMapper.readValue( in, aClass ); + } + catch ( IOException e ) { + throw new RuntimeException( "Unable to load JSON object from InputStream.", e ); + } + } + // SERIALIZATION + @Override public String toJsonString( Object obj ) { try { return objectMapper.writeValueAsString( obj ); @@ -227,6 +276,7 @@ public String toJsonString( Object obj ) { } } + @Override public String toPrettyJsonString( Object obj ) { try { return prettyPrintWriter.writeValueAsString( obj ); @@ -235,4 +285,10 @@ public String toPrettyJsonString( Object obj ) { throw new RuntimeException( "Unable to serialize object : " + obj, e ); } } + + @Override + public Object cloneJson( Object obj ) { + String string = this.toJsonString( obj ); + return this.jsonToObject( string ); + } } diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java index f0f59a46..641e4636 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java +++ b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java @@ -43,20 +43,6 @@ public static JsonUtil customJsonUtil( ObjectMapper mapper ) { return new JsonUtilImpl( mapper ); } - - /** - * Makes a deep copy of a Map object by converting it to a String and then - * back onto stock JSON objects. - * - * @param obj object tree to copy - * @return deep copy of the incoming obj - */ - public static Object cloneJson( Object obj ) { - - String string = util.toJsonString( obj ); - return util.jsonToObject( string ); - } - /** * Removes a key recursively from anywhere in a JSON document. * NOTE: mutates its input. @@ -112,6 +98,10 @@ public static Map javason( String javason ) return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); } + public static JsonUtil getDefaultJsonUtil() { + return util; + } + //// All the methods listed below are static passthrus to the JsonUtil interface public static Object jsonToObject( String json ) { return util.jsonToObject( json ); @@ -165,6 +155,32 @@ public static T classpathToType( String classPath, TypeReference typeRef return util.classpathToType( classPath, typeRef ); } + public static T classpathToType( String classPath, Class aClass ) { + return util.classpathToType( classPath, aClass ); + } + + public static T stringToType ( String json, TypeReference typeRef ) { + return util.stringToType( json, typeRef ); + } + + public static T stringToType( String json, Class aClass ) { + return util.stringToType( json, aClass ); + } + + public static T fileToType ( String filePath, TypeReference typeRef ) { + return util.fileToType( filePath, typeRef ); + } + public static T fileToType ( String filePath, Class aClass ) { + return util.fileToType( filePath, aClass ); + } + + public static T streamToType( InputStream in, TypeReference typeRef ) { + return util.streamToType( in, typeRef ); + } + public static T streamToType( InputStream in, Class aClass ) { + return util.streamToType( in, aClass ); + } + /** * Use the stringToType method instead. */ @@ -188,4 +204,17 @@ public static String toJsonString( Object obj ) { public static String toPrettyJsonString( Object obj ) { return util.toPrettyJsonString( obj ); } + + + /** + * Makes a deep copy of a Map object by converting it to a String and then + * back onto stock JSON objects. + * + * @param obj object tree to copy + * @return deep copy of the incoming obj + */ + public static Object cloneJson( Object obj ) { + // use the "configured" util for the serialize to String part + return util.cloneJson( obj ); + } }