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.json.JsonValue.json;
020import static org.forgerock.json.JsonValueFunctions.pointer;
021import static org.forgerock.util.Reject.checkNotNull;
022
023import java.util.ArrayList;
024import java.util.LinkedHashMap;
025import java.util.List;
026
027import org.forgerock.json.JsonPointer;
028import org.forgerock.json.JsonValue;
029
030/**
031 * An individual patch operation which is to be performed against a field within
032 * a resource. This class defines four core types of operation. The core
033 * operations are defined below and their behavior depends on the type of the
034 * field being targeted by the operation:
035 * <ul>
036 * <li>an object (Java {@code Map}) or primitive (Java {@code String},
037 * {@code Boolean}, or {@code Number}): these are considered to be
038 * <i>single-valued</i> fields
039 * <li>an array (Java {@code List}): these are considered to be
040 * <i>multi-valued</i> fields exhibiting either:
041 * <ul>
042 * <li><i>list</i> semantics - an ordered collection of potentially non-unique
043 * values, or
044 * <li><i>set</i> semantics - a collection of unique values whose ordering is
045 * implementation defined.
046 * </ul>
047 * The choice of semantic (list or set) associated with a multi-valued field is
048 * implementation defined, although it is usual for it to be defined using a
049 * schema.
050 * </ul>
051 * The four core patch operations are:
052 * <ul>
053 * <li>{@link #add(String, Object) add} - ensures that the targeted field
054 * contains the provided value(s). Missing parent fields will be created as
055 * needed. If the targeted field is already present and it is single-valued
056 * (i.e. not an array) then the existing value will be replaced. If the targeted
057 * field is already present and it is multi-valued (i.e. an array) then the
058 * behavior depends on whether the field is a <i>list</i> or a <i>set</i>:
059 * <ul>
060 * <li>list - the provided array of values will be appended to the existing list
061 * of values,
062 * <li>set - the provided array of values will be merged with the existing set
063 * of values and duplicates removed.
064 * </ul>
065 * Add operations which target a specific index of a multi-valued field are
066 * permitted as long as the field is a <i>list</i>. In this case the patch value
067 * must represent a single element of the list (i.e. it must not be an array of
068 * new elements) which will be inserted at the specified position. Indexed
069 * updates to <i>set</i>s are not permitted, although implementations may
070 * support the special index "-" which can be used to add a single value to a
071 * list or set.
072 * <li>{@link #remove(String, Object) remove} - ensures that the targeted field
073 * does not contain the provided value(s) if present. If no values are provided
074 * with the remove operation then the entire field will be removed if it is
075 * present. If the remove operation targets a single-valued field and a patch
076 * value is provided then it must match the existing value for it to be removed,
077 * otherwise the field is left unchanged. If the remove operation targets a
078 * multi-valued field then the behavior depends on whether the field is a
079 * <i>list</i> or a <i>set</i>:
080 * <ul>
081 * <li>list - the provided array of values will be removed from the existing
082 * list of values. Each value in the remove operation will result in at most one
083 * value being removed from the existing list. In other words, if the existing
084 * list contains a pair of duplicate values and both of them need to be removed,
085 * then the values must be include twice in the remove operation,
086 * <li>set - the provided array of values will be removed from the existing set
087 * of values.
088 * </ul>
089 * Remove operations which target a specific index of a multi-valued field are
090 * permitted as long as the field is a <i>list</i>. If a patch value is provided
091 * then it must match the existing value for it to be removed, otherwise the
092 * field is left unchanged. Indexed updates to <i>set</i>s are not permitted.
093 * <li>{@link #replace(String, Object) replace} - removes any existing value(s)
094 * of the targeted field and replaces them with the provided value(s). A replace
095 * operation is semantically equivalent to a {@code remove} followed by an
096 * {@code add}, except that indexed updates are not permitted regardless of
097 * whether or not the field is a list.
098 * <li>{@link #increment(String, Number) increment} - increments or decrements
099 * 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 */
111public 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}