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}