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