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   * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
18   *
19   * This software is dual-licensed under:
20   *
21   * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
22   *   later version;
23   * - the Apache Software License (ASL) version 2.0.
24   *
25   * The text of this file and of both licenses is available at the root of this
26   * project or, if you have the jar distribution, in directory META-INF/, under
27   * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
28   *
29   * Direct link to the sources:
30   *
31   * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
32   * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
33   */
34  
35  package com.linecorp.centraldogma.internal.jsonpatch;
36  
37  import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchUtil.encodeSegment;
38  import static java.util.Objects.requireNonNull;
39  
40  import java.io.IOException;
41  import java.util.HashMap;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.TreeSet;
47  
48  import com.fasterxml.jackson.annotation.JsonCreator;
49  import com.fasterxml.jackson.core.JsonGenerator;
50  import com.fasterxml.jackson.core.JsonPointer;
51  import com.fasterxml.jackson.databind.JsonMappingException;
52  import com.fasterxml.jackson.databind.JsonNode;
53  import com.fasterxml.jackson.databind.JsonSerializable;
54  import com.fasterxml.jackson.databind.SerializerProvider;
55  import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
56  import com.fasterxml.jackson.databind.node.ArrayNode;
57  import com.fasterxml.jackson.databind.node.JsonNodeType;
58  import com.fasterxml.jackson.databind.node.ObjectNode;
59  import com.google.common.annotations.VisibleForTesting;
60  import com.google.common.base.Equivalence;
61  import com.google.common.collect.ImmutableList;
62  import com.google.common.collect.Iterators;
63  import com.google.common.collect.Sets;
64  
65  import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
66  import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
67  import com.linecorp.centraldogma.internal.Jackson;
68  
69  /**
70   * Implementation of JSON Patch.
71   *
72   * <p><a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON
73   * Patch</a>, as its name implies, is an IETF draft describing a mechanism to
74   * apply a patch to any JSON value. This implementation covers all operations
75   * according to the specification; however, there are some subtle differences
76   * with regards to some operations which are covered in these operations'
77   * respective documentation.</p>
78   *
79   * <p>An example of a JSON Patch is as follows:</p>
80   *
81   * <pre>
82   *     [
83   *         {
84   *             "op": "add",
85   *             "path": "/-",
86   *             "value": {
87   *                 "productId": 19,
88   *                 "name": "Duvel",
89   *                 "type": "beer"
90   *             }
91   *         }
92   *     ]
93   * </pre>
94   *
95   * <p>This patch contains a single operation which adds an item at the end of
96   * an array. A JSON Patch can contain more than one operation; in this case, all
97   * operations are applied to the input JSON value in their order of appearance,
98   * until all operations are applied or an error condition is encountered.</p>
99   *
100  * <p>The main point where this implementation differs from the specification
101  * is initial JSON parsing. The draft says:</p>
102  *
103  * <pre>
104  *     Operation objects MUST have exactly one "op" member
105  * </pre>
106  *
107  * <p>and:</p>
108  *
109  * <pre>
110  *     Additionally, operation objects MUST have exactly one "path" member.
111  * </pre>
112  *
113  * <p>However, obeying these to the letter forces constraints on the JSON
114  * <b>parser</b>. Here, these constraints are not enforced, which means:</p>
115  *
116  * <pre>
117  *     [ { "op": "add", "op": "remove", "path": "/x" } ]
118  * </pre>
119  *
120  * <p>is parsed (as a {@code remove} operation, since it appears last).</p>
121  *
122  * <p><b>IMPORTANT NOTE:</b> the JSON Patch is supposed to be VALID when the
123  * constructor for this class ({@link JsonPatch#fromJson(JsonNode)} is used.</p>
124  */
125 public final class JsonPatch implements JsonSerializable {
126 
127     private static final Equivalence<JsonNode> EQUIVALENCE = JsonNumEquals.getInstance();
128     private static final JsonPointer EMPTY_JSON_POINTER = JsonPointer.compile("");
129     private static final JsonPointer END_OF_ARRAY_POINTER = JsonPointer.compile("/-");
130 
131     /**
132      * Static factory method to build a JSON Patch out of a JSON representation.
133      *
134      * @param node the JSON representation of the generated JSON Patch
135      * @return a JSON Patch
136      * @throws IOException input is not a valid JSON patch
137      * @throws NullPointerException input is null
138      */
139     public static JsonPatch fromJson(final JsonNode node) throws IOException {
140         requireNonNull(node, "node");
141         try {
142             return Jackson.treeToValue(node, JsonPatch.class);
143         } catch (JsonMappingException e) {
144             throw new JsonPatchConflictException("invalid JSON patch", e);
145         }
146     }
147 
148     /**
149      * Generates a JSON patch for transforming the source node into the target node.
150      *
151      * @param source the node to be patched
152      * @param target the expected result after applying the patch
153      * @param replaceMode the replace mode to be used
154      * @return the patch as a {@link JsonPatch}
155      */
156     public static JsonPatch generate(final JsonNode source, final JsonNode target, ReplaceMode replaceMode) {
157         requireNonNull(source, "source");
158         requireNonNull(target, "target");
159         final DiffProcessor processor = new DiffProcessor(replaceMode, () -> unchangedValues(source, target));
160         generateDiffs(processor, EMPTY_JSON_POINTER, source, target);
161         return processor.getPatch();
162     }
163 
164     private static void generateDiffs(final DiffProcessor processor, final JsonPointer pointer,
165                                       final JsonNode source, final JsonNode target) {
166 
167         if (EQUIVALENCE.equivalent(source, target)) {
168             return;
169         }
170 
171         final JsonNodeType sourceType = source.getNodeType();
172         final JsonNodeType targetType = target.getNodeType();
173 
174         /*
175          * Node types differ: generate a replacement operation.
176          */
177         if (sourceType != targetType) {
178             processor.valueReplaced(pointer, source, target);
179             return;
180         }
181 
182         /*
183          * If we reach this point, it means that both nodes are the same type,
184          * but are not equivalent.
185          *
186          * If this is not a container, generate a replace operation.
187          */
188         if (!source.isContainerNode()) {
189             processor.valueReplaced(pointer, source, target);
190             return;
191         }
192 
193         /*
194          * If we reach this point, both nodes are either objects or arrays;
195          * delegate.
196          */
197         if (sourceType == JsonNodeType.OBJECT) {
198             generateObjectDiffs(processor, pointer, (ObjectNode) source, (ObjectNode) target);
199         } else {
200             // array
201             generateArrayDiffs(processor, pointer, (ArrayNode) source, (ArrayNode) target);
202         }
203     }
204 
205     private static void generateObjectDiffs(final DiffProcessor processor, final JsonPointer pointer,
206                                             final ObjectNode source, final ObjectNode target) {
207 
208         final Set<String> sourceFields = new TreeSet<>();
209         Iterators.addAll(sourceFields, source.fieldNames());
210         final Set<String> targetFields = new TreeSet<>();
211         Iterators.addAll(targetFields, target.fieldNames());
212 
213         for (final String field : Sets.difference(sourceFields, targetFields)) {
214             processor.valueRemoved(pointer.append(JsonPointer.valueOf(encodeSegment(field))));
215         }
216 
217         for (final String field : Sets.difference(targetFields, sourceFields)) {
218             processor.valueAdded(pointer.append(JsonPointer.valueOf(encodeSegment(field))), target.get(field));
219         }
220 
221         for (final String field : Sets.intersection(sourceFields, targetFields)) {
222             generateDiffs(processor, pointer.append(JsonPointer.valueOf(encodeSegment(field))),
223                           source.get(field), target.get(field));
224         }
225     }
226 
227     private static void generateArrayDiffs(final DiffProcessor processor, final JsonPointer pointer,
228                                            final ArrayNode source, final ArrayNode target) {
229         final int sourceSize = source.size();
230         final int targetSize = target.size();
231         final int size = Math.min(sourceSize, targetSize);
232 
233         /*
234          * Source array is larger; in this case, elements are removed from the
235          * target; the index of removal is always the original arrays's length.
236          */
237         for (int index = size; index < sourceSize; index++) {
238             processor.valueRemoved(pointer.append(JsonPointer.valueOf("/" + size)));
239         }
240 
241         for (int index = 0; index < size; index++) {
242             generateDiffs(processor, pointer.append(JsonPointer.valueOf("/" + index)),
243                           source.get(index), target.get(index));
244         }
245 
246         // Deal with the destination array being larger...
247         for (int index = size; index < targetSize; index++) {
248             processor.valueAdded(pointer.append(END_OF_ARRAY_POINTER), target.get(index));
249         }
250     }
251 
252     @VisibleForTesting
253     static Map<JsonPointer, JsonNode> unchangedValues(final JsonNode source, final JsonNode target) {
254         final Map<JsonPointer, JsonNode> ret = new HashMap<>();
255         computeUnchanged(ret, EMPTY_JSON_POINTER, source, target);
256         return ret;
257     }
258 
259     private static void computeUnchanged(final Map<JsonPointer, JsonNode> ret, final JsonPointer pointer,
260                                          final JsonNode source, final JsonNode target) {
261         if (EQUIVALENCE.equivalent(source, target)) {
262             ret.put(pointer, target);
263             return;
264         }
265 
266         final JsonNodeType sourceType = source.getNodeType();
267         final JsonNodeType targetType = target.getNodeType();
268 
269         if (sourceType != targetType) {
270             return; // nothing in common
271         }
272 
273         // We know they are both the same type, so...
274 
275         switch (sourceType) {
276             case OBJECT:
277                 computeUnchangedObject(ret, pointer, source, target);
278                 break;
279             case ARRAY:
280                 computeUnchangedArray(ret, pointer, source, target);
281                 break;
282             default:
283                 /* nothing */
284         }
285     }
286 
287     private static void computeUnchangedObject(final Map<JsonPointer, JsonNode> ret, final JsonPointer pointer,
288                                                final JsonNode source, final JsonNode target) {
289         final Iterator<String> sourceFields = source.fieldNames();
290         while (sourceFields.hasNext()) {
291             final String name = sourceFields.next();
292             if (!target.has(name)) {
293                 continue;
294             }
295             computeUnchanged(ret, pointer.append(JsonPointer.valueOf(encodeSegment(name))),
296                              source.get(name), target.get(name));
297         }
298     }
299 
300     private static void computeUnchangedArray(final Map<JsonPointer, JsonNode> ret, final JsonPointer pointer,
301                                               final JsonNode source, final JsonNode target) {
302         final int size = Math.min(source.size(), target.size());
303         for (int i = 0; i < size; i++) {
304             computeUnchanged(ret, pointer.append(JsonPointer.valueOf("/" + i)),
305                              source.get(i), target.get(i));
306         }
307     }
308 
309     /**
310      * List of operations.
311      */
312     private final List<JsonPatchOperation> operations;
313 
314     /**
315      * Creates a new instance.
316      *
317      * @param operations the list of operations for this patch
318      * @see JsonPatchOperation
319      */
320     @JsonCreator
321     JsonPatch(final List<JsonPatchOperation> operations) {
322         this.operations = ImmutableList.copyOf(operations);
323     }
324 
325     /**
326      * Returns whether this patch is empty.
327      */
328     public boolean isEmpty() {
329         return operations.isEmpty();
330     }
331 
332     /**
333      * Returns the list of operations.
334      */
335     public List<JsonPatchOperation> operations() {
336         return operations;
337     }
338 
339     /**
340      * Applies this patch to a JSON value.
341      *
342      * @param node the value to apply the patch to
343      * @return the patched JSON value
344      * @throws JsonPatchConflictException failed to apply patch
345      * @throws NullPointerException input is null
346      */
347     public JsonNode apply(final JsonNode node) {
348         requireNonNull(node, "node");
349         JsonNode ret = node.deepCopy();
350         for (final JsonPatchOperation operation : operations) {
351             ret = operation.apply(ret);
352         }
353 
354         return ret;
355     }
356 
357     /**
358      * Converts this patch into JSON.
359      */
360     public ArrayNode toJson() {
361         return (ArrayNode) Jackson.valueToTree(this);
362     }
363 
364     @Override
365     public String toString() {
366         return operations.toString();
367     }
368 
369     @Override
370     public void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
371         jgen.writeStartArray();
372         for (final JsonPatchOperation op : operations) {
373             op.serialize(jgen, provider);
374         }
375         jgen.writeEndArray();
376     }
377 
378     @Override
379     public void serializeWithType(final JsonGenerator jgen,
380                                   final SerializerProvider provider, final TypeSerializer typeSer)
381             throws IOException {
382         serialize(jgen, provider);
383     }
384 }