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 Copyright [year] [name of copyright owner]". 13 * 14 * Copyright 2013-2015 ForgeRock AS. All rights reserved. 15 */ 16 17 package org.forgerock.json.resource; 18 19 import static org.forgerock.json.JsonValue.json; 20 import static org.forgerock.json.JsonValueFunctions.pointer; 21 import static org.forgerock.util.Reject.checkNotNull; 22 23 import java.util.ArrayList; 24 import java.util.LinkedHashMap; 25 import java.util.List; 26 27 import org.forgerock.json.JsonPointer; 28 import org.forgerock.json.JsonValue; 29 30 /** 31 * An individual patch operation which is to be performed against a field within 32 * a resource. This class defines four core types of operation. The core 33 * operations are defined below and their behavior depends on the type of the 34 * field being targeted by the operation: 35 * <ul> 36 * <li>an object (Java {@code Map}) or primitive (Java {@code String}, 37 * {@code Boolean}, or {@code Number}): these are considered to be 38 * <i>single-valued</i> fields 39 * <li>an array (Java {@code List}): these are considered to be 40 * <i>multi-valued</i> fields exhibiting either: 41 * <ul> 42 * <li><i>list</i> semantics - an ordered collection of potentially non-unique 43 * values, or 44 * <li><i>set</i> semantics - a collection of unique values whose ordering is 45 * implementation defined. 46 * </ul> 47 * The choice of semantic (list or set) associated with a multi-valued field is 48 * implementation defined, although it is usual for it to be defined using a 49 * schema. 50 * </ul> 51 * The four core patch operations are: 52 * <ul> 53 * <li>{@link #add(String, Object) add} - ensures that the targeted field 54 * contains the provided value(s). Missing parent fields will be created as 55 * needed. If the targeted field is already present and it is single-valued 56 * (i.e. not an array) then the existing value will be replaced. If the targeted 57 * field is already present and it is multi-valued (i.e. an array) then the 58 * behavior depends on whether the field is a <i>list</i> or a <i>set</i>: 59 * <ul> 60 * <li>list - the provided array of values will be appended to the existing list 61 * of values, 62 * <li>set - the provided array of values will be merged with the existing set 63 * of values and duplicates removed. 64 * </ul> 65 * Add operations which target a specific index of a multi-valued field are 66 * permitted as long as the field is a <i>list</i>. In this case the patch value 67 * must represent a single element of the list (i.e. it must not be an array of 68 * new elements) which will be inserted at the specified position. Indexed 69 * updates to <i>set</i>s are not permitted, although implementations may 70 * support the special index "-" which can be used to add a single value to a 71 * list or set. 72 * <li>{@link #remove(String, Object) remove} - ensures that the targeted field 73 * does not contain the provided value(s) if present. If no values are provided 74 * with the remove operation then the entire field will be removed if it is 75 * present. If the remove operation targets a single-valued field and a patch 76 * value is provided then it must match the existing value for it to be removed, 77 * otherwise the field is left unchanged. If the remove operation targets a 78 * multi-valued field then the behavior depends on whether the field is a 79 * <i>list</i> or a <i>set</i>: 80 * <ul> 81 * <li>list - the provided array of values will be removed from the existing 82 * list of values. Each value in the remove operation will result in at most one 83 * value being removed from the existing list. In other words, if the existing 84 * list contains a pair of duplicate values and both of them need to be removed, 85 * then the values must be include twice in the remove operation, 86 * <li>set - the provided array of values will be removed from the existing set 87 * of values. 88 * </ul> 89 * Remove operations which target a specific index of a multi-valued field are 90 * permitted as long as the field is a <i>list</i>. If a patch value is provided 91 * then it must match the existing value for it to be removed, otherwise the 92 * field is left unchanged. Indexed updates to <i>set</i>s are not permitted. 93 * <li>{@link #replace(String, Object) replace} - removes any existing value(s) 94 * of the targeted field and replaces them with the provided value(s). A replace 95 * operation is semantically equivalent to a {@code remove} followed by an 96 * {@code add}, except that indexed updates are not permitted regardless of 97 * whether or not the field is a list. 98 * <li>{@link #increment(String, Number) increment} - increments or decrements 99 * the targeted numerical field value(s) by the specified amount. If the amount 100 * is negative then the value(s) are decremented. It is an error to attempt to 101 * increment a field which does not contain a number or an array of numbers. It 102 * is also an error if the patch value is not a single value. 103 * </ul> 104 * <p> 105 * <b>NOTE:</b> this class does not define how field values will be matched, nor 106 * does it define whether a resource supports indexed based modifications, nor 107 * whether fields are single or multi-valued. Instead these matters are the 108 * responsibility of the resource provider and, in particular, the JSON schema 109 * being enforced for the targeted resource. 110 */ 111 public final class PatchOperation { 112 113 /** 114 * The name of the field which contains the target field in the JSON 115 * representation. 116 */ 117 public static final String FIELD_FIELD = "field"; 118 119 /** 120 * The name of the source field for copy and move operations. 121 */ 122 public static final String FIELD_FROM = "from"; 123 124 /** 125 * The name of the field which contains the type of patch operation in the 126 * JSON representation. 127 */ 128 public static final String FIELD_OPERATION = "operation"; 129 130 /** 131 * The name of the field which contains the operation value in the JSON 132 * representation. 133 */ 134 public static final String FIELD_VALUE = "value"; 135 136 /** 137 * The identifier used for "add" operations. 138 */ 139 public static final String OPERATION_ADD = "add"; 140 141 /** 142 * The identifier used for "increment" operations. 143 */ 144 public static final String OPERATION_INCREMENT = "increment"; 145 146 /** 147 * The identifier used for "remove" operations. 148 */ 149 public static final String OPERATION_REMOVE = "remove"; 150 151 /** 152 * The identifier used for "replace" operations. 153 */ 154 public static final String OPERATION_REPLACE = "replace"; 155 156 /** 157 * The identifier used for "move" operations. 158 */ 159 public static final String OPERATION_MOVE = "move"; 160 161 /** 162 * The identifier used for "copy" operations. 163 */ 164 public static final String OPERATION_COPY = "copy"; 165 166 /** 167 * The identifier used for "transform" operations. This is similar to an "add" or "replace" 168 * but the value may be treated as something other than a raw object. 169 */ 170 public static final String OPERATION_TRANSFORM = "transform"; 171 172 /** 173 * Creates a new "add" patch operation which will add the provided value(s) 174 * to the specified field. 175 * 176 * @param field 177 * The field to be added. 178 * @param value 179 * The new value(s) to be added, which may be a {@link JsonValue} 180 * or a JSON object, such as a {@code String}, {@code Map}, etc. 181 * @return The new patch operation. 182 * @throws NullPointerException 183 * If the value is {@code null}. 184 */ 185 public static PatchOperation add(final JsonPointer field, final Object value) { 186 return operation(OPERATION_ADD, field, value); 187 } 188 189 /** 190 * Creates a new "add" patch operation which will add the provided value(s) 191 * to the specified field. 192 * 193 * @param field 194 * The field to be added. 195 * @param value 196 * The new value(s) to be added, which may be a {@link JsonValue} 197 * or a JSON object, such as a {@code String}, {@code Map}, etc. 198 * @return The new patch operation. 199 * @throws NullPointerException 200 * If the value is {@code null}. 201 */ 202 public static PatchOperation add(final String field, final Object value) { 203 return add(new JsonPointer(field), value); 204 } 205 206 /** 207 * Creates a new "increment" patch operation which will increment the 208 * value(s) of the specified field by the amount provided. 209 * 210 * @param field 211 * The field to be incremented. 212 * @param amount 213 * The amount to be added or removed (if negative) from the 214 * field's value(s). 215 * @return The new patch operation. 216 * @throws NullPointerException 217 * If the amount is {@code null}. 218 */ 219 public static PatchOperation increment(final JsonPointer field, final Number amount) { 220 return operation(OPERATION_INCREMENT, field, amount); 221 } 222 223 /** 224 * Creates a new "increment" patch operation which will increment the 225 * value(s) of the specified field by the amount provided. 226 * 227 * @param field 228 * The field to be incremented. 229 * @param amount 230 * The amount to be added or removed (if negative) from the 231 * field's value(s). 232 * @return The new patch operation. 233 * @throws NullPointerException 234 * If the amount is {@code null}. 235 */ 236 public static PatchOperation increment(final String field, final Number amount) { 237 return increment(new JsonPointer(field), amount); 238 } 239 240 /** 241 * Creates a new "remove" patch operation which will remove the specified 242 * field. 243 * 244 * @param field 245 * The field to be removed. 246 * @return The new patch operation. 247 */ 248 public static PatchOperation remove(final JsonPointer field) { 249 return remove(field, null); 250 } 251 252 /** 253 * Creates a new "remove" patch operation which will remove the provided 254 * value(s) from the specified field. 255 * 256 * @param field 257 * The field to be removed. 258 * @param value 259 * The value(s) to be removed, which may be a {@link JsonValue} 260 * or a JSON object, such as a {@code String}, {@code Map}, etc. 261 * @return The new patch operation. 262 */ 263 public static PatchOperation remove(final JsonPointer field, final Object value) { 264 return operation(OPERATION_REMOVE, field, value); 265 } 266 267 /** 268 * Creates a new "remove" patch operation which will remove the specified 269 * field. 270 * 271 * @param field 272 * The field to be removed. 273 * @return The new patch operation. 274 */ 275 public static PatchOperation remove(final String field) { 276 return remove(new JsonPointer(field)); 277 } 278 279 /** 280 * Creates a new "remove" patch operation which will remove the provided 281 * value(s) from the specified field. 282 * 283 * @param field 284 * The field to be removed. 285 * @param value 286 * The value(s) to be removed, which may be a {@link JsonValue} 287 * or a JSON object, such as a {@code String}, {@code Map}, etc. 288 * @return The new patch operation. 289 */ 290 public static PatchOperation remove(final String field, final Object value) { 291 return remove(new JsonPointer(field), value); 292 } 293 294 /** 295 * Creates a new "replace" patch operation which will replace the value(s) 296 * of the specified field with the provided value(s). 297 * 298 * @param field 299 * The field to be replaced. 300 * @param value 301 * The new value(s) for the field, which may be a 302 * {@link JsonValue} or a JSON object, such as a {@code String}, 303 * {@code Map}, etc. 304 * @return The new patch operation. 305 */ 306 public static PatchOperation replace(final JsonPointer field, final Object value) { 307 return operation(OPERATION_REPLACE, field, value); 308 } 309 310 /** 311 * Creates a new "replace" patch operation which will replace the value(s) 312 * of the specified field with the provided value(s). 313 * 314 * @param field 315 * The field to be replaced. 316 * @param value 317 * The new value(s) for the field, which may be a 318 * {@link JsonValue} or a JSON object, such as a {@code String}, 319 * {@code Map}, etc. 320 * @return The new patch operation. 321 */ 322 public static PatchOperation replace(final String field, final Object value) { 323 return replace(new JsonPointer(field), value); 324 } 325 326 /** 327 * Creates a new "move" patch operation which will move the value found at `from` to `path`. 328 * 329 * @param from 330 * The field to be moved. 331 * @param field 332 * The destination path for the moved value 333 * @return The new patch operation. 334 * @throws NullPointerException 335 * If the from or path is {@code null}. 336 */ 337 public static PatchOperation move(final JsonPointer from, final JsonPointer field) { 338 return operation(OPERATION_MOVE, from, field); 339 } 340 341 /** 342 * Creates a new "move" patch operation which will move the value found at `from` to `path`. 343 * 344 * @param from 345 * The field to be moved. 346 * @param field 347 * The destination path for the moved value 348 * @return The new patch operation. 349 * @throws NullPointerException 350 * If the from or path is {@code null}. 351 */ 352 public static PatchOperation move(final String from, final String field) { 353 return operation(OPERATION_MOVE, new JsonPointer(from), new JsonPointer(field)); 354 } 355 356 /** 357 * Creates a new "copy" patch operation which will copy the value found at `from` to `path`. 358 * 359 * @param from 360 * The field to be copied. 361 * @param field 362 * The destination path for the copied value 363 * @return The new patch operation. 364 * @throws NullPointerException 365 * If the from or path is {@code null}. 366 */ 367 public static PatchOperation copy(final JsonPointer from, final JsonPointer field) { 368 return operation(OPERATION_COPY, from, field); 369 } 370 371 /** 372 * Creates a new "copy" patch operation which will copy the value found at `from` to `path`. 373 * 374 * @param from 375 * The field to be copied. 376 * @param field 377 * The destination path for the copied value 378 * @return The new patch operation. 379 * @throws NullPointerException 380 * If the from or path is {@code null}. 381 */ 382 public static PatchOperation copy(final String from, final String field) { 383 return operation(OPERATION_COPY, new JsonPointer(from), new JsonPointer(field)); 384 } 385 386 /** 387 * Creates a new "transform" patch operation which sets the value at field based on a 388 * transformation. 389 * 390 * @param field 391 * The field to be set. 392 * @param transform 393 * The transform to be used to set the field value. 394 * @return The new patch operation. 395 * @throws NullPointerException 396 * If the transform is {@code null}. 397 */ 398 public static PatchOperation transform(final JsonPointer field, final Object transform) { 399 return operation(OPERATION_TRANSFORM, field, transform); 400 } 401 402 /** 403 * Creates a new "transform" patch operation which sets the value at field based on a 404 * transformation. 405 * 406 * @param field 407 * The field to be set. 408 * @param transform 409 * The transform to be used to set the field value. 410 * @return The new patch operation. 411 * @throws NullPointerException 412 * If the transform is {@code null}. 413 */ 414 public static PatchOperation transform(final String field, final Object transform) { 415 return operation(OPERATION_TRANSFORM, new JsonPointer(field), transform); 416 } 417 418 /** 419 * Creates a new patch operation having the specified operation type, field, 420 * and value(s). 421 * 422 * @param operation 423 * The type of patch operation to be performed. 424 * @param field 425 * The field targeted by the patch operation. 426 * @param value 427 * The possibly {@code null} value for the patch operation, which 428 * may be a {@link JsonValue} or a JSON object, such as a 429 * {@code String}, {@code Map}, etc. 430 * @return The new patch operation. 431 */ 432 public static PatchOperation operation(final String operation, final JsonPointer field, final Object value) { 433 return new PatchOperation(operation, field, null, json(value), null); 434 } 435 436 /** 437 * Creates a new patch operation having the specified operation type, from and field. 438 * 439 * @param operation 440 * The type of patch operation to be performed. 441 * @param from 442 * The source field for the patch operation. 443 * @param field 444 * The field targeted by the patch operation. 445 * @return The new patch operation. 446 * @throws IllegalArgumentException 447 * If the operation is not move or copy. 448 */ 449 private static PatchOperation operation(final String operation, final JsonPointer from, final JsonPointer field) { 450 return new PatchOperation(operation, field, from, json(null), null); 451 } 452 453 /** 454 * Creates a new patch operation having the specified operation type, field, 455 * and value(s). 456 * 457 * @param operation 458 * The type of patch operation to be performed. 459 * @param field 460 * The field targeted by the patch operation. 461 * @param value 462 * The possibly {@code null} value for the patch operation, which 463 * may be a {@link JsonValue} or a JSON object, such as a 464 * {@code String}, {@code Map}, etc. 465 * @return The new patch operation. 466 */ 467 public static PatchOperation operation(final String operation, final String field, final Object value) { 468 return operation(operation, new JsonPointer(field), value); 469 } 470 471 /** 472 * Returns a deep copy of the provided patch operation. This method may be 473 * used in cases where the immutability of the underlying JSON value cannot 474 * be guaranteed. 475 * 476 * @param operation 477 * The patch operation to be defensively copied. 478 * @return A deep copy of the provided patch operation. 479 */ 480 public static PatchOperation copyOf(final PatchOperation operation) { 481 return new PatchOperation( 482 operation.getOperation(), 483 operation.getField(), 484 operation.getFrom(), 485 operation.getValue().copy(), 486 operation.toJsonValue().copy()); 487 } 488 489 /** 490 * Parses the provided JSON content as a patch operation. 491 * 492 * @param json 493 * The patch operation to be parsed. 494 * @return The parsed patch operation. 495 * @throws BadRequestException 496 * If the JSON value is not a JSON patch operation. 497 */ 498 public static PatchOperation valueOf(final JsonValue json) throws BadRequestException { 499 if (!json.isMap()) { 500 throw new BadRequestException( 501 "The request could not be processed because the provided " 502 + "content is not a valid JSON patch"); 503 } 504 try { 505 return new PatchOperation(json.get(FIELD_OPERATION).asString(), json.get(FIELD_FIELD).as(pointer()), 506 json.get(FIELD_FROM).as(pointer()), json.get(FIELD_VALUE), json); 507 } catch (final Exception e) { 508 throw new BadRequestException( 509 "The request could not be processed because the provided " 510 + "content is not a valid JSON patch: " + e.getMessage(), e); 511 } 512 } 513 514 /** 515 * Parses the provided JSON content as a list of patch operations. 516 * 517 * @param json 518 * The list of patch operations to be parsed. 519 * @return The list of parsed patch operations. 520 * @throws BadRequestException 521 * If the JSON value is not a list of JSON patch operations. 522 */ 523 public static List<PatchOperation> valueOfList(final JsonValue json) throws BadRequestException { 524 if (!json.isList()) { 525 throw new BadRequestException( 526 "The request could not be processed because the provided " 527 + "content is not a JSON array of patch operations"); 528 } 529 final List<PatchOperation> patch = new ArrayList<>(json.size()); 530 for (final JsonValue operation : json) { 531 patch.add(valueOf(operation)); 532 } 533 return patch; 534 } 535 536 private final JsonPointer field; 537 private final JsonPointer from; 538 private final String operation; 539 private final JsonValue value; 540 private JsonValue json; 541 542 private PatchOperation(final String operation, final JsonPointer field, final JsonPointer from, 543 final JsonValue value, final JsonValue json) { 544 checkNotNull(operation, "Cannot instantiate PatchOperation with null 'operation' value"); 545 checkNotNull(field, "Cannot instantiate PatchOperation with null 'field' value"); 546 checkNotNull(value, "Cannot instantiate PatchOperation with null 'value' value"); 547 548 this.operation = operation; 549 checkOperationType(); 550 this.field = field; 551 this.value = value; 552 this.from = from; 553 this.json = json; 554 555 if (isAdd() || isIncrement() || isReplace() || isTransform()) { 556 if (value.isNull()) { 557 throw new NullPointerException("No value field provided for '" + operation + "' operation"); 558 } 559 if (from != null) { 560 throw new IllegalArgumentException("'" + operation + "' does not accept from field"); 561 } 562 if (isIncrement() && !value.isNumber()) { 563 throw new IllegalArgumentException("Non-numeric value provided for increment operation"); 564 } 565 } else if (isRemove()) { 566 if (from != null) { 567 throw new IllegalArgumentException("'" + operation + "' does not accept from field"); 568 } 569 } else if (isCopy() || isMove()) { 570 if (from == null || from.isEmpty()) { 571 throw new NullPointerException("No from field provided for '" + operation + "' operation"); 572 } 573 if (value.isNotNull()) { 574 throw new IllegalArgumentException("'" + operation + "' does not accept value field"); 575 } 576 } 577 } 578 579 private void checkOperationType() { 580 if (!isAdd() && !isRemove() && !isIncrement() && !isReplace() && !isTransform() && !isMove() && !isCopy()) { 581 throw new IllegalArgumentException("Invalid patch operation type " + operation); 582 } 583 } 584 585 /** 586 * Returns the field targeted by the patch operation. 587 * 588 * @return The field targeted by the patch operation. 589 */ 590 public JsonPointer getField() { 591 return field; 592 } 593 594 /** 595 * Returns the source field for move and copy operations. 596 * 597 * @return The source field for move and copy operations. 598 */ 599 public JsonPointer getFrom() { 600 return from; 601 } 602 603 /** 604 * Returns the type of patch operation to be performed. 605 * 606 * @return The type of patch operation to be performed. 607 */ 608 public String getOperation() { 609 return operation; 610 } 611 612 /** 613 * Returns the value for the patch operation. The return value may be 614 * a JSON value whose value is {@code null}. 615 * 616 * @return The nullable value for the patch operation. 617 */ 618 public JsonValue getValue() { 619 return value; 620 } 621 622 /** 623 * Returns {@code true} if this is an "add" patch operation. 624 * 625 * @return {@code true} if this is an "add" patch operation. 626 */ 627 public boolean isAdd() { 628 return is(OPERATION_ADD); 629 } 630 631 /** 632 * Returns {@code true} if this is an "increment" patch operation. 633 * 634 * @return {@code true} if this is an "increment" patch operation. 635 */ 636 public boolean isIncrement() { 637 return is(OPERATION_INCREMENT); 638 } 639 640 /** 641 * Returns {@code true} if this is an "remove" patch operation. 642 * 643 * @return {@code true} if this is an "remove" patch operation. 644 */ 645 public boolean isRemove() { 646 return is(OPERATION_REMOVE); 647 } 648 649 /** 650 * Returns {@code true} if this is an "replace" patch operation. 651 * 652 * @return {@code true} if this is an "replace" patch operation. 653 */ 654 public boolean isReplace() { 655 return is(OPERATION_REPLACE); 656 } 657 658 /** 659 * Returns {@code true} if this is a "move" patch operation. 660 * 661 * @return {@code true} if this is a "move" patch operation. 662 */ 663 public boolean isMove() { 664 return is(OPERATION_MOVE); 665 } 666 667 /** 668 * Returns {@code true} if this is a "copy" patch operation. 669 * 670 * @return {@code true} if this is a "copy" patch operation. 671 */ 672 public boolean isCopy() { 673 return is(OPERATION_COPY); 674 } 675 676 /** 677 * Returns {@code true} if this is a "transform" patch operation. 678 * 679 * @return {@code true} if this is a "transform" patch operation. 680 */ 681 public boolean isTransform() { 682 return is(OPERATION_TRANSFORM); 683 } 684 685 /** 686 * Returns a JSON value representation of this patch operation. 687 * 688 * @return A JSON value representation of this patch operation. 689 */ 690 public JsonValue toJsonValue() { 691 if (json == null) { 692 json = new JsonValue(new LinkedHashMap<>()); 693 json.put(FIELD_OPERATION, operation); 694 json.put(FIELD_FIELD, field.toString()); 695 if (from != null) { 696 json.put(FIELD_FROM, from.toString()); 697 } 698 if (value.isNotNull()) { 699 json.put(FIELD_VALUE, value.getObject()); 700 } 701 } 702 return json; 703 } 704 705 @Override 706 public String toString() { 707 return toJsonValue().toString(); 708 } 709 710 private boolean is(final String type) { 711 return operation.equalsIgnoreCase(type); 712 } 713 }