001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2011-2016 ForgeRock AS.
015 */
016
017package org.forgerock.json;
018
019import static org.forgerock.json.JsonValueFunctions.pointer;
020
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.List;
025
026import org.forgerock.util.Reject;
027
028/**
029 * Processes partial modifications to JSON values.
030 */
031public final class JsonPatch {
032
033    /**
034     * Internet media type for the JSON Patch format.
035     */
036    public static final String MEDIA_TYPE = "application/json-patch";
037
038    /**
039     * Path to the "op" attribute of a patch entry. Required.
040     */
041    public static final JsonPointer OP_PTR = new JsonPointer("/op");
042
043    /**
044     * Path to the "path" attribute of a patch entry. Required.
045     */
046    public static final JsonPointer PATH_PTR = new JsonPointer("/path");
047
048    /**
049     * Path to the "from" attribute of a patch entry. Required only for "move" and "copy"
050     * operations. Ignored for all others.
051     */
052    public static final JsonPointer FROM_PTR = new JsonPointer("/from");
053
054    /**
055     * Path to the "value" attribute of a patch entry. Required for "add", "replace" and
056     * "test" operations; Ignored for all others.
057     *
058     * This is public to allow for alternate implementations of {@link JsonPatchValueTransformer}.
059     */
060    public static final JsonPointer VALUE_PTR = new JsonPointer("/value");
061
062    /**
063     * Default transform for patch values; Conforms to RFC6902.
064     */
065    private static final JsonPatchValueTransformer DEFAULT_TRANSFORM =
066        new JsonPatchValueTransformer() {
067            public Object getTransformedValue(JsonValue target, JsonValue op) {
068                if (op.get(JsonPatch.VALUE_PTR) != null) {
069                    return op.get(JsonPatch.VALUE_PTR).getObject();
070                }
071                throw new JsonValueException(op, "expecting a value member");
072            }
073        };
074
075    /**
076     * Compares two JSON values, and produces a JSON Patch value, which contains the
077     * operations necessary to modify the {@code original} value to arrive at the
078     * {@code target} value.
079     *
080     * @param original the original value.
081     * @param target the intended target value.
082     * @return the resulting JSON Patch value.
083     * @throws NullPointerException if either of {@code original} or {@code target} are {@code null}.
084     */
085    public static JsonValue diff(JsonValue original, JsonValue target) {
086        final List<Object> result = new ArrayList<>();
087        if (differentTypes(original, target)) { // different types cause a replace
088            result.add(op("replace", original.getPointer(), target));
089        } else if (original.isMap()) {
090            for (String key : original.keys()) {
091                if (target.isDefined(key)) { // target also has the property
092                    JsonValue diff = diff(original.get(key), target.get(key)); // recursively compare properties
093                    if (diff.size() > 0) {
094                        result.addAll(diff.asList()); // add diff results
095                    }
096                } else { // property is missing in target
097                    result.add(op("remove", original.getPointer().child(key), null));
098                }
099            }
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}