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