1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.internal;
18  
19  import static com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT;
20  import static com.google.common.base.Preconditions.checkArgument;
21  import static java.util.Objects.requireNonNull;
22  
23  import java.io.File;
24  import java.io.IOError;
25  import java.io.IOException;
26  import java.io.Writer;
27  import java.time.Instant;
28  import java.util.Iterator;
29  import java.util.Set;
30  
31  import com.fasterxml.jackson.core.JsonFactory;
32  import com.fasterxml.jackson.core.JsonGenerator;
33  import com.fasterxml.jackson.core.JsonParseException;
34  import com.fasterxml.jackson.core.JsonParser;
35  import com.fasterxml.jackson.core.JsonProcessingException;
36  import com.fasterxml.jackson.core.TreeNode;
37  import com.fasterxml.jackson.core.io.JsonStringEncoder;
38  import com.fasterxml.jackson.core.io.SegmentedStringWriter;
39  import com.fasterxml.jackson.core.type.TypeReference;
40  import com.fasterxml.jackson.core.util.DefaultIndenter;
41  import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
42  import com.fasterxml.jackson.databind.JsonMappingException;
43  import com.fasterxml.jackson.databind.JsonNode;
44  import com.fasterxml.jackson.databind.Module;
45  import com.fasterxml.jackson.databind.ObjectMapper;
46  import com.fasterxml.jackson.databind.SerializationFeature;
47  import com.fasterxml.jackson.databind.jsontype.NamedType;
48  import com.fasterxml.jackson.databind.module.SimpleModule;
49  import com.fasterxml.jackson.databind.node.JsonNodeType;
50  import com.fasterxml.jackson.databind.node.NullNode;
51  import com.fasterxml.jackson.databind.node.ObjectNode;
52  import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
53  import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer;
54  import com.google.common.collect.ImmutableList;
55  import com.google.common.collect.Iterables;
56  import com.jayway.jsonpath.Configuration;
57  import com.jayway.jsonpath.Configuration.Defaults;
58  import com.jayway.jsonpath.JsonPath;
59  import com.jayway.jsonpath.Option;
60  import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
61  import com.jayway.jsonpath.spi.json.JsonProvider;
62  import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
63  import com.jayway.jsonpath.spi.mapper.MappingProvider;
64  
65  import com.linecorp.centraldogma.common.QueryExecutionException;
66  import com.linecorp.centraldogma.common.QuerySyntaxException;
67  
68  public final class Jackson {
69  
70      private static final ObjectMapper compactMapper = new ObjectMapper();
71      private static final ObjectMapper prettyMapper = new ObjectMapper();
72  
73      static {
74          // Pretty-print the JSON when serialized via the mapper.
75          compactMapper.disable(SerializationFeature.INDENT_OUTPUT);
76          prettyMapper.enable(SerializationFeature.INDENT_OUTPUT);
77          // Sort the attributes when serialized via the mapper.
78          compactMapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
79          prettyMapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
80  
81          registerModules(new SimpleModule().addSerializer(Instant.class, InstantSerializer.INSTANCE)
82                                            .addDeserializer(Instant.class, InstantDeserializer.INSTANT));
83      }
84  
85      private static final JsonFactory compactFactory = new JsonFactory(compactMapper);
86      private static final JsonFactory prettyFactory = new JsonFactory(prettyMapper);
87      private static final Configuration jsonPathCfg =
88              Configuration.builder()
89                           .jsonProvider(new JacksonJsonNodeJsonProvider())
90                           .mappingProvider(new JacksonMappingProvider(prettyMapper))
91                           .build();
92  
93      static {
94          // If the json-path library is shaded, its transitive dependency 'json-smart' should not be required.
95          // Override the default configuration so that json-path does not attempt to load the json-smart classes.
96          if (Configuration.class.getPackage().getName().endsWith(".shaded.jsonpath")) {
97              Configuration.setDefaults(new Defaults() {
98                  @Override
99                  public JsonProvider jsonProvider() {
100                     return jsonPathCfg.jsonProvider();
101                 }
102 
103                 @Override
104                 public Set<Option> options() {
105                     return jsonPathCfg.getOptions();
106                 }
107 
108                 @Override
109                 public MappingProvider mappingProvider() {
110                     return jsonPathCfg.mappingProvider();
111                 }
112             });
113         }
114     }
115 
116     public static final NullNode nullNode = NullNode.instance;
117 
118     public static void registerModules(Module... modules) {
119         compactMapper.registerModules(modules);
120         prettyMapper.registerModules(modules);
121     }
122 
123     public static void registerSubtypes(NamedType... subtypes) {
124         compactMapper.registerSubtypes(subtypes);
125         prettyMapper.registerSubtypes(subtypes);
126     }
127 
128     public static void registerSubtypes(Class<?>... subtypes) {
129         compactMapper.registerSubtypes(subtypes);
130         prettyMapper.registerSubtypes(subtypes);
131     }
132 
133     public static <T> T readValue(String data, Class<T> type) throws JsonParseException, JsonMappingException {
134         try {
135             return compactMapper.readValue(data, type);
136         } catch (JsonParseException | JsonMappingException e) {
137             throw e;
138         } catch (IOException e) {
139             throw new IOError(e);
140         }
141     }
142 
143     public static <T> T readValue(byte[] data, Class<T> type) throws JsonParseException, JsonMappingException {
144         try {
145             return compactMapper.readValue(data, type);
146         } catch (JsonParseException | JsonMappingException e) {
147             throw e;
148         } catch (IOException e) {
149             throw new IOError(e);
150         }
151     }
152 
153     public static <T> T readValue(File file, Class<T> type) throws JsonParseException, JsonMappingException {
154         try {
155             return compactMapper.readValue(file, type);
156         } catch (JsonParseException | JsonMappingException e) {
157             throw e;
158         } catch (IOException e) {
159             throw new IOError(e);
160         }
161     }
162 
163     public static <T> T readValue(String data, TypeReference<T> typeReference)
164             throws JsonParseException, JsonMappingException {
165         try {
166             return compactMapper.readValue(data, typeReference);
167         } catch (JsonParseException | JsonMappingException e) {
168             throw e;
169         } catch (IOException e) {
170             throw new IOError(e);
171         }
172     }
173 
174     public static <T> T readValue(byte[] data, TypeReference<T> typeReference)
175             throws JsonParseException, JsonMappingException {
176         try {
177             return compactMapper.readValue(data, typeReference);
178         } catch (JsonParseException | JsonMappingException e) {
179             throw e;
180         } catch (IOException e) {
181             throw new IOError(e);
182         }
183     }
184 
185     public static <T> T readValue(File file, TypeReference<T> typeReference) throws IOException {
186         return compactMapper.readValue(file, typeReference);
187     }
188 
189     public static <T> T readValue(JsonParser jp, TypeReference<T> typeReference) throws IOException {
190         return compactMapper.readValue(jp, typeReference);
191     }
192 
193     public static JsonNode readTree(String data) throws JsonParseException {
194         try {
195             return compactMapper.readTree(data);
196         } catch (JsonParseException e) {
197             throw e;
198         } catch (IOException e) {
199             throw new IOError(e);
200         }
201     }
202 
203     public static JsonNode readTree(byte[] data) throws JsonParseException {
204         try {
205             return compactMapper.readTree(data);
206         } catch (JsonParseException e) {
207             throw e;
208         } catch (IOException e) {
209             throw new IOError(e);
210         }
211     }
212 
213     public static byte[] writeValueAsBytes(Object value) throws JsonProcessingException {
214         return compactMapper.writeValueAsBytes(value);
215     }
216 
217     public static String writeValueAsString(Object value) throws JsonProcessingException {
218         return compactMapper.writeValueAsString(value);
219     }
220 
221     public static String writeValueAsPrettyString(Object value) throws JsonProcessingException {
222         // XXX(trustin): prettyMapper.writeValueAsString() does not respect the custom pretty printer
223         //               set via ObjectMapper.setDefaultPrettyPrinter() for an unknown reason, so we
224         //               create a generator manually and set the pretty printer explicitly.
225         final JsonFactory factory = prettyMapper.getFactory();
226         final SegmentedStringWriter sw = new SegmentedStringWriter(factory._getBufferRecycler());
227         try {
228             final JsonGenerator g = prettyMapper.getFactory().createGenerator(sw);
229             g.setPrettyPrinter(new PrettyPrinterImpl());
230             prettyMapper.writeValue(g, value);
231             return sw.getAndClear();
232         } catch (JsonProcessingException e) {
233             throw e;
234         } catch (IOException e) {
235             throw new IOError(e);
236         }
237     }
238 
239     public static <T extends JsonNode> T valueToTree(Object value) {
240         return compactMapper.valueToTree(value);
241     }
242 
243     public static <T> T treeToValue(TreeNode node, Class<T> valueType)
244             throws JsonParseException, JsonMappingException {
245         try {
246             return compactMapper.treeToValue(node, valueType);
247         } catch (JsonParseException | JsonMappingException e) {
248             throw e;
249         } catch (JsonProcessingException e) {
250             // Should never reach here.
251             throw new IllegalStateException(e);
252         }
253     }
254 
255     public static <T> T convertValue(Object fromValue, Class<T> toValueType) {
256         return compactMapper.convertValue(fromValue, toValueType);
257     }
258 
259     public static <T> T convertValue(Object fromValue, TypeReference<T> toValueTypeRef) {
260         return compactMapper.convertValue(fromValue, toValueTypeRef);
261     }
262 
263     public static JsonGenerator createGenerator(Writer writer) throws IOException {
264         return compactFactory.createGenerator(writer);
265     }
266 
267     public static JsonGenerator createPrettyGenerator(Writer writer) throws IOException {
268         final JsonGenerator generator = prettyFactory.createGenerator(writer);
269         generator.useDefaultPrettyPrinter();
270         return generator;
271     }
272 
273     public static String textValue(JsonNode node, String defaultValue) {
274         return node != null && node.getNodeType() == JsonNodeType.STRING ? node.textValue() : defaultValue;
275     }
276 
277     public static JsonNode extractTree(JsonNode jsonNode, Iterable<String> jsonPaths) {
278         for (String jsonPath : jsonPaths) {
279             jsonNode = extractTree(jsonNode, jsonPath);
280         }
281         return jsonNode;
282     }
283 
284     public static JsonNode extractTree(JsonNode jsonNode, String jsonPath) {
285         requireNonNull(jsonNode, "jsonNode");
286         requireNonNull(jsonPath, "jsonPath");
287 
288         final JsonPath compiledJsonPath;
289         try {
290             compiledJsonPath = JsonPath.compile(jsonPath);
291         } catch (Exception e) {
292             throw new QuerySyntaxException("invalid JSON path: " + jsonPath, e);
293         }
294 
295         try {
296             return JsonPath.parse(jsonNode, jsonPathCfg)
297                            .read(compiledJsonPath, JsonNode.class);
298         } catch (Exception e) {
299             throw new QueryExecutionException("JSON path evaluation failed: " + jsonPath, e);
300         }
301     }
302 
303     public static String escapeText(String text) {
304         final JsonStringEncoder enc = JsonStringEncoder.getInstance();
305         return new String(enc.quoteAsString(text));
306     }
307 
308     public static JsonNode mergeTree(JsonNode... jsonNodes) {
309         return mergeTree(ImmutableList.copyOf(requireNonNull(jsonNodes, "jsonNodes")));
310     }
311 
312     public static JsonNode mergeTree(Iterable<JsonNode> jsonNodes) {
313         requireNonNull(jsonNodes, "jsonNodes");
314         final int size = Iterables.size(jsonNodes);
315         checkArgument(size > 0, "jsonNodes is empty.");
316         final Iterator<JsonNode> it = jsonNodes.iterator();
317         final JsonNode first = it.next();
318         JsonNode merged = first.deepCopy();
319 
320         final StringBuilder fieldNameAppender = new StringBuilder("/");
321         while (it.hasNext()) {
322             final JsonNode addition = it.next();
323             merged = traverse(merged, addition, fieldNameAppender, true, true);
324         }
325 
326         if (size > 2) {
327             // Traverse once more to find the mismatched value between the first and the merged node.
328             traverse(first, merged, fieldNameAppender, false, true);
329         }
330         return merged;
331     }
332 
333     private static JsonNode traverse(JsonNode base, JsonNode update, StringBuilder fieldNameAppender,
334                                      boolean isMerging, boolean isRoot) {
335         if (base.isObject() && update.isObject()) {
336             final ObjectNode baseObject = (ObjectNode) base;
337             final Iterator<String> fieldNames = update.fieldNames();
338             while (fieldNames.hasNext()) {
339                 final String fieldName = fieldNames.next();
340                 final JsonNode baseValue = baseObject.get(fieldName);
341                 final JsonNode updateValue = update.get(fieldName);
342 
343                 if (baseValue == null || baseValue.isNull() || updateValue.isNull()) {
344                     if (isMerging) {
345                         baseObject.set(fieldName, updateValue);
346                     }
347                     continue;
348                 }
349 
350                 final int length = fieldNameAppender.length();
351                 // Append the filed name and traverse the child.
352                 fieldNameAppender.append(fieldName);
353                 fieldNameAppender.append('/');
354                 final JsonNode traversed =
355                         traverse(baseValue, updateValue, fieldNameAppender, isMerging, false);
356                 if (isMerging) {
357                     baseObject.set(fieldName, traversed);
358                 }
359                 // Remove the appended filed name above.
360                 fieldNameAppender.delete(length, fieldNameAppender.length());
361             }
362 
363             return base;
364         }
365 
366         if (isRoot || (base.getNodeType() != update.getNodeType() && (!base.isNull() || !update.isNull()))) {
367             throw new QueryExecutionException("Failed to merge tree. " + fieldNameAppender +
368                                               " type: " + update.getNodeType() +
369                                               " (expected: " + (isRoot ? OBJECT : base.getNodeType()) + ')');
370         }
371 
372         return update;
373     }
374 
375     private Jackson() {}
376 
377     private static class PrettyPrinterImpl extends DefaultPrettyPrinter {
378         private static final long serialVersionUID = 8408886209309852098L;
379 
380         // The default object indenter uses platform-dependent line separator, so we define one
381         // with a fixed separator (\n).
382         private static final Indenter objectIndenter = new DefaultIndenter("  ", "\n");
383 
384         @SuppressWarnings("AssignmentToSuperclassField")
385         PrettyPrinterImpl() {
386             _objectFieldValueSeparatorWithSpaces = ": ";
387             _objectIndenter = objectIndenter;
388         }
389     }
390 }