View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions Copyrighted [year] [name of copyright owner]".
13   *
14   * Copyright 2011-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.json;
18  
19  import static org.forgerock.json.JsonValueFunctions.pointer;
20  
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.List;
25  
26  import org.forgerock.util.Reject;
27  
28  /**
29   * Processes partial modifications to JSON values.
30   */
31  public final class JsonPatch {
32  
33      /**
34       * Internet media type for the JSON Patch format.
35       */
36      public static final String MEDIA_TYPE = "application/json-patch";
37  
38      /**
39       * Path to the "op" attribute of a patch entry. Required.
40       */
41      public static final JsonPointer OP_PTR = new JsonPointer("/op");
42  
43      /**
44       * Path to the "path" attribute of a patch entry. Required.
45       */
46      public static final JsonPointer PATH_PTR = new JsonPointer("/path");
47  
48      /**
49       * Path to the "from" attribute of a patch entry. Required only for "move" and "copy"
50       * operations. Ignored for all others.
51       */
52      public static final JsonPointer FROM_PTR = new JsonPointer("/from");
53  
54      /**
55       * Path to the "value" attribute of a patch entry. Required for "add", "replace" and
56       * "test" operations; Ignored for all others.
57       *
58       * This is public to allow for alternate implementations of {@link JsonPatchValueTransformer}.
59       */
60      public static final JsonPointer VALUE_PTR = new JsonPointer("/value");
61  
62      /**
63       * Default transform for patch values; Conforms to RFC6902.
64       */
65      private static final JsonPatchValueTransformer DEFAULT_TRANSFORM =
66          new JsonPatchValueTransformer() {
67              public Object getTransformedValue(JsonValue target, JsonValue op) {
68                  if (op.get(JsonPatch.VALUE_PTR) != null) {
69                      return op.get(JsonPatch.VALUE_PTR).getObject();
70                  }
71                  throw new JsonValueException(op, "expecting a value member");
72              }
73          };
74  
75      /**
76       * Compares two JSON values, and produces a JSON Patch value, which contains the
77       * operations necessary to modify the {@code original} value to arrive at the
78       * {@code target} value.
79       *
80       * @param original the original value.
81       * @param target the intended target value.
82       * @return the resulting JSON Patch value.
83       * @throws NullPointerException if either of {@code original} or {@code target} are {@code null}.
84       */
85      public static JsonValue diff(JsonValue original, JsonValue target) {
86          final List<Object> result = new ArrayList<>();
87          if (differentTypes(original, target)) { // different types cause a replace
88              result.add(op("replace", original.getPointer(), target));
89          } else if (original.isMap()) {
90              for (String key : original.keys()) {
91                  if (target.isDefined(key)) { // target also has the property
92                      JsonValue diff = diff(original.get(key), target.get(key)); // recursively compare properties
93                      if (diff.size() > 0) {
94                          result.addAll(diff.asList()); // add diff results
95                      }
96                  } else { // property is missing in target
97                      result.add(op("remove", original.getPointer().child(key), null));
98                  }
99              }
100             for (String key : target.keys()) {
101                 if (!original.isDefined(key)) { // property is in target, not in original
102                     result.add(op("add", original.getPointer().child(key), target.get(key)));
103                 }
104             }
105         } else if (original.isList()) {
106             boolean replace = false;
107             if (original.size() != target.size()) {
108                 replace = true;
109             } else {
110                 Iterator<JsonValue> i1 = original.iterator();
111                 Iterator<JsonValue> i2 = target.iterator();
112                 while (i1.hasNext() && i2.hasNext()) {
113                     if (diff(i1.next(), i2.next()).size() > 0) { // recursively compare elements
114                         replace = true;
115                         break;
116                     }
117                 }
118             }
119             if (replace) { // replace list entirely
120                 result.add(op("replace", original.getPointer(), target));
121             }
122         } else if (!original.isNull() && !original.getObject().equals(target.getObject())) { // simple value comparison
123             result.add(op("replace", original.getPointer(), target));
124         }
125         return new JsonValue(result);
126     }
127 
128     /**
129      * Compares two JSON values, and returns whether the two objects are identical.  Fails fast in that a
130      * {@code false} is returned as soon as a difference is detected.
131      *
132      * @param value a value.
133      * @param other another value.
134      * @return whether the two inputs are equal.
135      * @throws NullPointerException if either of {@code value} or {@code other} are {@code null}.
136      * @throws IllegalArgumentException if the {@link JsonValue} contains non-JSON primitive values.
137      */
138     public static boolean isEqual(JsonValue value, JsonValue other) {
139         Reject.ifFalse(isJsonPrimitive(value) && isJsonPrimitive(other),
140                 "JsonPatch#isEqual only supports recognizable JSON primitives");
141         if (differentTypes(value, other)) {
142             return false;
143         }
144         if (value.size() != other.size()) {
145             return false;
146         }
147         if (value.isMap()) {
148             // only need test that other has same keys with same values as value as they are the same size at this point
149             for (String key : value.keys()) {
150                 if (!other.isDefined(key) // other is missing the property
151                             || !isEqual(value.get(key), other.get(key))) { // recursively compare properties
152                     return false;
153                 }
154             }
155         } else if (value.isList()) {
156             Iterator<JsonValue> i1 = value.iterator();
157             Iterator<JsonValue> i2 = other.iterator();
158             while (i1.hasNext() && i2.hasNext()) {
159                 if (!isEqual(i1.next(), i2.next())) { // recursively compare elements
160                     return false;
161                 }
162             }
163         } else if (!value.isNull() && !value.getObject().equals(other.getObject())) { // simple value comparison
164             return false;
165         }
166         return true;
167     }
168 
169     private static boolean isJsonPrimitive(JsonValue value) {
170         return value.isNull() || value.isBoolean() || value.isMap() || value.isList() || value.isNumber()
171                 || value.isString();
172     }
173 
174     /**
175      * Returns {@code true} if the type contained by {@code v1} is different than the type
176      * contained by {@code v2}.
177      * <p>
178      * Note: If an unexpected (non-JSON) type is encountered, this method returns
179      * {@code true}, triggering a change in the resulting patch.
180      */
181     private static boolean differentTypes(JsonValue v1, JsonValue v2) {
182         return !(v1.isNull() && v2.isNull())
183                 && !(v1.isMap() && v2.isMap())
184                 && !(v1.isList() && v2.isList())
185                 && !(v1.isString() && v2.isString())
186                 && !(v1.isNumber() && v2.isNumber())
187                 && !(v1.isBoolean() && v2.isBoolean());
188     }
189 
190     private static HashMap<String, Object> op(String op, JsonPointer pointer, JsonValue value) {
191         HashMap<String, Object> result = new HashMap<String, Object>();
192         result.put(OP_PTR.leaf(), op);
193         result.put(PATH_PTR.leaf(), pointer.toString());
194         if (value != null) {
195             result.put(VALUE_PTR.leaf(), value.copy().getObject());
196         }
197         return result;
198     }
199 
200     /**
201      * Applies a set of modifications in a JSON patch value to an original value, resulting
202      * in the intended target value. In the event of a failure, this method does not revert
203      * any modifications applied up to the point of failure.
204      *
205      * @param original the original value on which to apply the modifications.
206      * @param patch the JSON Patch value, specifying the modifications to apply to the original value.
207      * @throws JsonValueException if application of the patch failed.
208      */
209     public static void patch(JsonValue original, JsonValue patch) {
210         patch(original, patch, DEFAULT_TRANSFORM);
211     }
212 
213     /**
214      * Applies a set of modifications in a JSON patch value to an original value, resulting
215      * in the intended target value. In the event of a failure, this method does not revert
216      * any modifications applied up to the point of failure.
217      *
218      * @param original the original value on which to apply the modifications.
219      * @param patch the JSON Patch value, specifying the modifications to apply to the original value.
220      * @param transform a custom transform used to determine the target value.
221      * @throws JsonValueException if application of the patch failed.
222      */
223     public static void patch(JsonValue original, JsonValue patch, JsonPatchValueTransformer transform) {
224         for (JsonValue operation : patch.required().expect(List.class)) {
225             if (!operation.isDefined("op")) {
226                 throw new JsonValueException(operation, "op not specified");
227             }
228             PatchOperation op = PatchOperation.valueOf(operation.get(OP_PTR));
229             if (op == null) {
230                 throw new JsonValueException(operation, "invalid op specified");
231             }
232             op.execute(original, operation, transform);
233         }
234     }
235 
236     private enum PatchOperation {
237         ADD {
238             // http://tools.ietf.org/html/rfc6902#section-4.1
239             @Override
240             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
241                 JsonPointer modifyPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
242                 JsonValue parent = parentValue(modifyPath, original);
243                 if (parent == null) {
244                     // patch specifies a new root object
245                     if (original.getObject() != null) {
246                         throw new JsonValueException(operation, "root value already exists");
247                     }
248                     original.setObject(transform.getTransformedValue(original, operation));
249                 } else {
250                     try {
251                         if (parent.isList()) {
252                             try {
253                                 // if the path points to an array index then we should insert the value
254                                 Integer index = Integer.valueOf(modifyPath.leaf());
255                                 parent.add(index, transform.getTransformedValue(original, operation));
256                             } catch (Exception e) {
257                                 // leaf is not an array index, replace value
258                                 parent.add(modifyPath.leaf(), transform.getTransformedValue(original, operation));
259                             }
260                         } else if (original.get(modifyPath) != null && original.get(modifyPath).isList()) {
261                             // modifyPath does not indicate an index, use the whole object
262                             JsonValue target = original.get(modifyPath);
263                             target.asList().add(transform.getTransformedValue(original, operation));
264                         } else {
265                             // this will replace the value even if present
266                             parent.add(modifyPath.leaf(), transform.getTransformedValue(original, operation));
267                         }
268                     } catch (JsonException je) {
269                         throw new JsonValueException(operation, je);
270                     }
271                 }
272             }
273         },
274         REMOVE {
275             //http://tools.ietf.org/html/rfc6902#section-4.2
276             @Override
277             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
278                 JsonPointer modifyPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
279                 JsonValue parent = parentValue(modifyPath, original);
280                 String leaf = modifyPath.leaf();
281                 if (parent == null) {
282                     // patch specifies root object
283                     original.setObject(null);
284                 } else {
285                     if (!parent.isDefined(leaf)) {
286                         throw new JsonValueException(operation, "value to remove not found");
287                     }
288                     try {
289                         parent.remove(leaf);
290                     } catch (JsonException je) {
291                         throw new JsonValueException(operation, je);
292                     }
293                 }
294             }
295         },
296         REPLACE {
297             //http://tools.ietf.org/html/rfc6902#section-4.3
298             @Override
299             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
300                 JsonPointer modifyPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
301                 JsonValue parent = parentValue(modifyPath, original);
302                 if (parent != null) {
303                     // replacing a child
304                     String leaf = modifyPath.leaf();
305                     if (!parent.isDefined(leaf)) {
306                         throw new JsonValueException(operation, "value to replace not found");
307                     }
308                     parent.put(leaf, transform.getTransformedValue(original, operation));
309                 } else {
310                     // replacing the root value itself
311                     original.setObject(transform.getTransformedValue(original, operation));
312                 }
313             }
314         },
315         MOVE {
316             // http://tools.ietf.org/html/rfc6902#section-4.4
317             @Override
318             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
319                 JsonPointer sourcePath = operation.get(FROM_PTR).expect(String.class).as(pointer());
320                 JsonPointer destPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
321                 JsonValue sourceParent = parentValue(sourcePath, original);
322                 if (sourceParent == null) {
323                     throw new JsonValueException(operation, "cannot move root object");
324                 }
325                 JsonValue object = sourceParent.get(sourcePath.leaf());
326                 JsonValue destParent = parentValue(destPath, original);
327                 if (destParent == null) {
328                     // replacing root object with moved object
329                     original.setObject(object);
330                 } else {
331                     sourceParent.remove(sourcePath.leaf());
332                     destParent.put(destPath.leaf(), object);
333                 }
334             }
335         },
336         COPY {
337             // http://tools.ietf.org/html/rfc6902#section-4.5
338             @Override
339             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
340                 JsonPointer sourcePath = operation.get(FROM_PTR).expect(String.class).as(pointer());
341                 JsonPointer destPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
342                 JsonValue sourceParent = parentValue(sourcePath, original);
343                 JsonValue object = sourceParent.get(sourcePath.leaf());
344                 JsonValue destParent = parentValue(destPath, original);
345                 if (destParent == null) {
346                     // replacing root object with copied object
347                     original.setObject(object);
348                 } else {
349                     destParent.put(destPath.leaf(), object);
350                 }
351             }
352         },
353         TEST {
354             // http://tools.ietf.org/html/rfc6902#section-4.6
355             @Override
356             void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
357                 JsonPointer testPath = operation.get(PATH_PTR).expect(String.class).as(pointer());
358                 JsonValue testTarget = parentValue(testPath, original).get(testPath.leaf());
359                 JsonValue testValue = new JsonValue(transform.getTransformedValue(original, operation));
360 
361                 if (diff(testTarget, testValue).asList().size() > 0) {
362                     throw new JsonValueException(operation, "test failed");
363                 }
364             }
365         };
366 
367         void execute(JsonValue original, JsonValue operation, JsonPatchValueTransformer transform) {
368             throw new JsonValueException(original, "unsupported operation");
369         }
370 
371         static PatchOperation valueOf(JsonValue op) {
372             return valueOf(op.expect(String.class).asString().toUpperCase());
373         }
374     }
375 
376     /**
377      * Returns the parent value of the value identified by the JSON pointer.
378      *
379      * @param pointer the pointer to the value whose parent value is to be returned.
380      * @param target the JSON value against which to resolve the JSON pointer.
381      * @return the parent value of the value identified by the JSON pointer.
382      * @throws JsonException if the parent value could not be found.
383      */
384     private static JsonValue parentValue(JsonPointer pointer, JsonValue target) {
385         JsonValue result = null;
386         JsonPointer parent = pointer.parent();
387         if (parent != null) {
388             result = target.get(parent);
389             if (result == null) {
390                 throw new JsonException("parent value not found");
391             }
392         }
393         return result;
394     }
395 
396     // prevent construction
397     private JsonPatch() {
398     }
399 }