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 }