View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.caravan.pipeline.impl;
21  
22  import io.wcm.caravan.pipeline.JsonPipelineInputException;
23  import io.wcm.caravan.pipeline.JsonPipelineOutputException;
24  
25  import java.io.IOException;
26  import java.io.StringWriter;
27  
28  import rx.functions.Func1;
29  
30  import com.fasterxml.jackson.core.JsonFactory;
31  import com.fasterxml.jackson.core.JsonGenerator;
32  import com.fasterxml.jackson.core.JsonParser;
33  import com.fasterxml.jackson.databind.DeserializationFeature;
34  import com.fasterxml.jackson.databind.JsonNode;
35  import com.fasterxml.jackson.databind.ObjectMapper;
36  import com.fasterxml.jackson.databind.node.ArrayNode;
37  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
38  import com.fasterxml.jackson.databind.node.ObjectNode;
39  
40  /**
41   * Contains common conversion functions between Jackson JSON nodes, Strings and Objects.
42   */
43  public final class JacksonFunctions {
44  
45    private static ObjectMapper objectMapper = new ObjectMapper()
46    .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
47    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
48  
49    private static JsonFactory jsonFactory = new JsonFactory(objectMapper)
50    .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) // this is mostly useful to write JSON like { a: 123 } in unit tests
51    .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); // this was also just added for convenience.
52  
53    private static JsonNodeFactory nodeFactory = JsonNodeFactory.withExactBigDecimals(false);
54  
55    // TODO: all these features should be made configurable
56  
57    private JacksonFunctions() {
58    }
59    // the basic conversions that don't need a type can simply be defined as static methods
60    // when they are used as functions for Observable#map, you can reference them RxJackson::stringToNode
61  
62  
63    /**
64     * Parse the given JSON string using the default factory
65     * @param jsonString a string with a valid JSON document
66     * @return either a {@link ObjectNode} or {@link ArrayNode}, depending on the input
67     * @throws JsonPipelineInputException if the input is not valid JSON
68     */
69    public static JsonNode stringToNode(String jsonString) {
70      JsonNode node = null;
71      try {
72        node = jsonFactory.createParser(jsonString).readValueAsTree();
73      }
74      catch (IOException ex) {
75        throw new JsonPipelineInputException(500, "Failed to parse JSON: " + jsonString, ex);
76      }
77      return node;
78    };
79  
80    /**
81     * Parse the given JSON string using the default factory
82     * @param jsonString a string with a valid JSON document
83     * @return a {@link ObjectNode}
84     * @throws JsonPipelineInputException if the input is not valid a JSON *Object*
85     */
86    public static ObjectNode stringToObjectNode(String jsonString) {
87      ObjectNode node = null;
88      try {
89        node = jsonFactory.createParser(jsonString).readValueAsTree();
90      }
91      catch (IOException | ClassCastException ex) {
92        throw new JsonPipelineInputException(500, "Failed to parse JSON object from: " + jsonString, ex);
93      }
94      return node;
95    };
96  
97    /**
98     * Serializes the given JSON node using the default factory
99     * @param node
100    * @return JSON string
101    * @throws JsonPipelineOutputException if the given node can not be serialized
102    */
103   public static String nodeToString(JsonNode node) {
104     if (node.isMissingNode()) {
105       throw new JsonPipelineOutputException(
106           "Received a MissingNode from the JSON pipeline output. Please do not serialize this result as a String and handle MissingNode in your client.");
107     }
108     try {
109       StringWriter writer = new StringWriter();
110       JsonGenerator generator = jsonFactory.createGenerator(writer);
111       generator.writeObject(node);
112       return writer.toString();
113     }
114     catch (IOException ex) {
115       throw new JsonPipelineOutputException("Failed to serialize JsonNode. This was quite unexpected.", ex);
116     }
117   };
118 
119   /**
120    * Creates a new JSON object with a single property
121    * @param targetProperty the name of the single property
122    * @param propertyValue the value of the
123    * @return the new ObjectNode
124    */
125   public static ObjectNode wrapInObject(String targetProperty, JsonNode propertyValue) {
126     ObjectNode objectNode = nodeFactory.objectNode();
127     objectNode.set(targetProperty, propertyValue);
128     return objectNode;
129   }
130 
131   /**
132    * @return an empty ObjectNode
133    */
134   public static ObjectNode emptyObject() {
135     return nodeFactory.objectNode();
136   }
137 
138   /**
139    * Create a JSON tree with the same structure as the given map
140    * @param object that can be properly mapped to JSON with Jackson (e.g. a POJO or Map)
141    * @return an {@link ObjectNode}
142    * @throws JsonPipelineInputException if the input is not valid JSON
143    */
144   public static JsonNode pojoToNode(Object object) {
145     JsonNode node = null;
146     try {
147       node = objectMapper.valueToTree(object);
148     }
149     catch (IllegalArgumentException ex) {
150       throw new JsonPipelineInputException(500, "Failed to create JSONNode from object of class " + object.getClass().getName(), ex);
151     }
152     return node;
153   };
154 
155 
156   /**
157    * Serializes the given POJO into a JSON string using the default factory
158    * @param pojo an object that can be serialized with Jackson's default {@link ObjectMapper}
159    * @return JSON string
160    * @throws JsonPipelineOutputException if the given object can not be serialized
161    */
162   public static String pojoToString(Object pojo) {
163     try {
164       StringWriter writer = new StringWriter();
165       objectMapper.writeValue(writer, pojo);
166       return writer.toString();
167     }
168     catch (IOException ex) {
169       throw new JsonPipelineOutputException("Failed to serialize entity to JSON string", ex);
170     }
171   };
172 
173   // the functions that convert to a Java type depend on the target class, that's why we can't make them as plain
174   // static functions. Instead they return a lambda that can be passed to Observable#map
175 
176   /**
177    * Returns a function that will instantiate a bean of the given class, with values copied from the JSON string
178    * @param targetType the POJO class that matches the expected JSON structure
179    * @return a function that takes a JSON string and returns a new instance of the target type
180    * @throws JsonPipelineInputException if the JSON could not be parsed, or the object not instantiated
181    */
182   public static <T> Func1<String, T> stringToPojo(Class<T> targetType) {
183     return jsonString -> {
184       try {
185         return jsonFactory.createParser(jsonString).readValueAs(targetType);
186       }
187       catch (IOException ex) {
188         throw new JsonPipelineInputException(500, "Failed to create entity of " + targetType.getName() + " from JSON", ex);
189       }
190     };
191   }
192 
193   /**
194    * Returns a function that will instantiate a bean of the given class, with values copied from the JSON string
195    * @param targetType the POJO class that matches the expected JSON structure
196    * @return a function that takes a JsonNode, and returns a new instance of the target type
197    * @throws JsonPipelineInputException if the JSON could not be parsed, or the object not instantiated
198    */
199   public static <T> Func1<JsonNode, T> nodeToPojo(Class<T> targetType) {
200     return jsonNode -> {
201       String jsonString = nodeToString(jsonNode);
202       return stringToPojo(targetType).call(jsonString);
203     };
204   }
205 
206 }