View Javadoc
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 }