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}