PatchOperation.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2015 ForgeRock AS. All rights reserved.
 */

package org.forgerock.json.resource;

import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValueFunctions.pointer;
import static org.forgerock.util.Reject.checkNotNull;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;

/**
 * An individual patch operation which is to be performed against a field within
 * a resource. This class defines four core types of operation. The core
 * operations are defined below and their behavior depends on the type of the
 * field being targeted by the operation:
 * <ul>
 * <li>an object (Java {@code Map}) or primitive (Java {@code String},
 * {@code Boolean}, or {@code Number}): these are considered to be
 * <i>single-valued</i> fields
 * <li>an array (Java {@code List}): these are considered to be
 * <i>multi-valued</i> fields exhibiting either:
 * <ul>
 * <li><i>list</i> semantics - an ordered collection of potentially non-unique
 * values, or
 * <li><i>set</i> semantics - a collection of unique values whose ordering is
 * implementation defined.
 * </ul>
 * The choice of semantic (list or set) associated with a multi-valued field is
 * implementation defined, although it is usual for it to be defined using a
 * schema.
 * </ul>
 * The four core patch operations are:
 * <ul>
 * <li>{@link #add(String, Object) add} - ensures that the targeted field
 * contains the provided value(s). Missing parent fields will be created as
 * needed. If the targeted field is already present and it is single-valued
 * (i.e. not an array) then the existing value will be replaced. If the targeted
 * field is already present and it is multi-valued (i.e. an array) then the
 * behavior depends on whether the field is a <i>list</i> or a <i>set</i>:
 * <ul>
 * <li>list - the provided array of values will be appended to the existing list
 * of values,
 * <li>set - the provided array of values will be merged with the existing set
 * of values and duplicates removed.
 * </ul>
 * Add operations which target a specific index of a multi-valued field are
 * permitted as long as the field is a <i>list</i>. In this case the patch value
 * must represent a single element of the list (i.e. it must not be an array of
 * new elements) which will be inserted at the specified position. Indexed
 * updates to <i>set</i>s are not permitted, although implementations may
 * support the special index "-" which can be used to add a single value to a
 * list or set.
 * <li>{@link #remove(String, Object) remove} - ensures that the targeted field
 * does not contain the provided value(s) if present. If no values are provided
 * with the remove operation then the entire field will be removed if it is
 * present. If the remove operation targets a single-valued field and a patch
 * value is provided then it must match the existing value for it to be removed,
 * otherwise the field is left unchanged. If the remove operation targets a
 * multi-valued field then the behavior depends on whether the field is a
 * <i>list</i> or a <i>set</i>:
 * <ul>
 * <li>list - the provided array of values will be removed from the existing
 * list of values. Each value in the remove operation will result in at most one
 * value being removed from the existing list. In other words, if the existing
 * list contains a pair of duplicate values and both of them need to be removed,
 * then the values must be include twice in the remove operation,
 * <li>set - the provided array of values will be removed from the existing set
 * of values.
 * </ul>
 * Remove operations which target a specific index of a multi-valued field are
 * permitted as long as the field is a <i>list</i>. If a patch value is provided
 * then it must match the existing value for it to be removed, otherwise the
 * field is left unchanged. Indexed updates to <i>set</i>s are not permitted.
 * <li>{@link #replace(String, Object) replace} - removes any existing value(s)
 * of the targeted field and replaces them with the provided value(s). A replace
 * operation is semantically equivalent to a {@code remove} followed by an
 * {@code add}, except that indexed updates are not permitted regardless of
 * whether or not the field is a list.
 * <li>{@link #increment(String, Number) increment} - increments or decrements
 * the targeted numerical field value(s) by the specified amount. If the amount
 * is negative then the value(s) are decremented. It is an error to attempt to
 * increment a field which does not contain a number or an array of numbers. It
 * is also an error if the patch value is not a single value.
 * </ul>
 * <p>
 * <b>NOTE:</b> this class does not define how field values will be matched, nor
 * does it define whether a resource supports indexed based modifications, nor
 * whether fields are single or multi-valued. Instead these matters are the
 * responsibility of the resource provider and, in particular, the JSON schema
 * being enforced for the targeted resource.
 */
public final class PatchOperation {

    /**
     * The name of the field which contains the target field in the JSON
     * representation.
     */
    public static final String FIELD_FIELD = "field";

    /**
     * The name of the source field for copy and move operations.
     */
    public static final String FIELD_FROM = "from";

    /**
     * The name of the field which contains the type of patch operation in the
     * JSON representation.
     */
    public static final String FIELD_OPERATION = "operation";

    /**
     * The name of the field which contains the operation value in the JSON
     * representation.
     */
    public static final String FIELD_VALUE = "value";

    /**
     * The identifier used for "add" operations.
     */
    public static final String OPERATION_ADD = "add";

    /**
     * The identifier used for "increment" operations.
     */
    public static final String OPERATION_INCREMENT = "increment";

    /**
     * The identifier used for "remove" operations.
     */
    public static final String OPERATION_REMOVE = "remove";

    /**
     * The identifier used for "replace" operations.
     */
    public static final String OPERATION_REPLACE = "replace";

    /**
     * The identifier used for "move" operations.
     */
    public static final String OPERATION_MOVE = "move";

    /**
     * The identifier used for "copy" operations.
     */
    public static final String OPERATION_COPY = "copy";

    /**
     * The identifier used for "transform" operations.  This is similar to an "add" or "replace"
     * but the value may be treated as something other than a raw object.
     */
    public static final String OPERATION_TRANSFORM = "transform";

    /**
     * Creates a new "add" patch operation which will add the provided value(s)
     * to the specified field.
     *
     * @param field
     *            The field to be added.
     * @param value
     *            The new value(s) to be added, which may be a {@link JsonValue}
     *            or a JSON object, such as a {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the value is {@code null}.
     */
    public static PatchOperation add(final JsonPointer field, final Object value) {
        return operation(OPERATION_ADD, field, value);
    }

    /**
     * Creates a new "add" patch operation which will add the provided value(s)
     * to the specified field.
     *
     * @param field
     *            The field to be added.
     * @param value
     *            The new value(s) to be added, which may be a {@link JsonValue}
     *            or a JSON object, such as a {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the value is {@code null}.
     */
    public static PatchOperation add(final String field, final Object value) {
        return add(new JsonPointer(field), value);
    }

    /**
     * Creates a new "increment" patch operation which will increment the
     * value(s) of the specified field by the amount provided.
     *
     * @param field
     *            The field to be incremented.
     * @param amount
     *            The amount to be added or removed (if negative) from the
     *            field's value(s).
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the amount is {@code null}.
     */
    public static PatchOperation increment(final JsonPointer field, final Number amount) {
        return operation(OPERATION_INCREMENT, field, amount);
    }

    /**
     * Creates a new "increment" patch operation which will increment the
     * value(s) of the specified field by the amount provided.
     *
     * @param field
     *            The field to be incremented.
     * @param amount
     *            The amount to be added or removed (if negative) from the
     *            field's value(s).
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the amount is {@code null}.
     */
    public static PatchOperation increment(final String field, final Number amount) {
        return increment(new JsonPointer(field), amount);
    }

    /**
     * Creates a new "remove" patch operation which will remove the specified
     * field.
     *
     * @param field
     *            The field to be removed.
     * @return The new patch operation.
     */
    public static PatchOperation remove(final JsonPointer field) {
        return remove(field, null);
    }

    /**
     * Creates a new "remove" patch operation which will remove the provided
     * value(s) from the specified field.
     *
     * @param field
     *            The field to be removed.
     * @param value
     *            The value(s) to be removed, which may be a {@link JsonValue}
     *            or a JSON object, such as a {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation remove(final JsonPointer field, final Object value) {
        return operation(OPERATION_REMOVE, field, value);
    }

    /**
     * Creates a new "remove" patch operation which will remove the specified
     * field.
     *
     * @param field
     *            The field to be removed.
     * @return The new patch operation.
     */
    public static PatchOperation remove(final String field) {
        return remove(new JsonPointer(field));
    }

    /**
     * Creates a new "remove" patch operation which will remove the provided
     * value(s) from the specified field.
     *
     * @param field
     *            The field to be removed.
     * @param value
     *            The value(s) to be removed, which may be a {@link JsonValue}
     *            or a JSON object, such as a {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation remove(final String field, final Object value) {
        return remove(new JsonPointer(field), value);
    }

    /**
     * Creates a new "replace" patch operation which will replace the value(s)
     * of the specified field with the provided value(s).
     *
     * @param field
     *            The field to be replaced.
     * @param value
     *            The new value(s) for the field, which may be a
     *            {@link JsonValue} or a JSON object, such as a {@code String},
     *            {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation replace(final JsonPointer field, final Object value) {
        return operation(OPERATION_REPLACE, field, value);
    }

    /**
     * Creates a new "replace" patch operation which will replace the value(s)
     * of the specified field with the provided value(s).
     *
     * @param field
     *            The field to be replaced.
     * @param value
     *            The new value(s) for the field, which may be a
     *            {@link JsonValue} or a JSON object, such as a {@code String},
     *            {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation replace(final String field, final Object value) {
        return replace(new JsonPointer(field), value);
    }

    /**
     * Creates a new "move" patch operation which will move the value found at `from` to `path`.
     *
     * @param from
     *            The field to be moved.
     * @param field
     *            The destination path for the moved value
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the from or path is {@code null}.
     */
    public static PatchOperation move(final JsonPointer from, final JsonPointer field) {
        return operation(OPERATION_MOVE, from, field);
    }

    /**
     * Creates a new "move" patch operation which will move the value found at `from` to `path`.
     *
     * @param from
     *            The field to be moved.
     * @param field
     *            The destination path for the moved value
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the from or path is {@code null}.
     */
    public static PatchOperation move(final String from, final String field) {
        return operation(OPERATION_MOVE, new JsonPointer(from), new JsonPointer(field));
    }

    /**
     * Creates a new "copy" patch operation which will copy the value found at `from` to `path`.
     *
     * @param from
     *            The field to be copied.
     * @param field
     *            The destination path for the copied value
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the from or path is {@code null}.
     */
    public static PatchOperation copy(final JsonPointer from, final JsonPointer field) {
        return operation(OPERATION_COPY, from, field);
    }

    /**
     * Creates a new "copy" patch operation which will copy the value found at `from` to `path`.
     *
     * @param from
     *            The field to be copied.
     * @param field
     *            The destination path for the copied value
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the from or path is {@code null}.
     */
    public static PatchOperation copy(final String from, final String field) {
        return operation(OPERATION_COPY, new JsonPointer(from), new JsonPointer(field));
    }

    /**
     * Creates a new "transform" patch operation which sets the value at field based on a
     * transformation.
     *
     * @param field
     *            The field to be set.
     * @param transform
     *            The transform to be used to set the field value.
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the transform is {@code null}.
     */
    public static PatchOperation transform(final JsonPointer field, final Object transform) {
        return operation(OPERATION_TRANSFORM, field, transform);
    }

    /**
     * Creates a new "transform" patch operation which sets the value at field based on a
     * transformation.
     *
     * @param field
     *            The field to be set.
     * @param transform
     *            The transform to be used to set the field value.
     * @return The new patch operation.
     * @throws NullPointerException
     *             If the transform is {@code null}.
     */
    public static PatchOperation transform(final String field, final Object transform) {
        return operation(OPERATION_TRANSFORM, new JsonPointer(field), transform);
    }

    /**
     * Creates a new patch operation having the specified operation type, field,
     * and value(s).
     *
     * @param operation
     *            The type of patch operation to be performed.
     * @param field
     *            The field targeted by the patch operation.
     * @param value
     *            The possibly {@code null} value for the patch operation, which
     *            may be a {@link JsonValue} or a JSON object, such as a
     *            {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation operation(final String operation, final JsonPointer field, final Object value) {
        return new PatchOperation(operation, field, null, json(value), null);
    }

    /**
     * Creates a new patch operation having the specified operation type, from and field.
     *
     * @param operation
     *            The type of patch operation to be performed.
     * @param from
     *            The source field for the patch operation.
     * @param field
     *            The field targeted by the patch operation.
     * @return The new patch operation.
     * @throws IllegalArgumentException
     *             If the operation is not move or copy.
     */
    private static PatchOperation operation(final String operation, final JsonPointer from, final JsonPointer field) {
        return new PatchOperation(operation, field, from, json(null), null);
    }

    /**
     * Creates a new patch operation having the specified operation type, field,
     * and value(s).
     *
     * @param operation
     *            The type of patch operation to be performed.
     * @param field
     *            The field targeted by the patch operation.
     * @param value
     *            The possibly {@code null} value for the patch operation, which
     *            may be a {@link JsonValue} or a JSON object, such as a
     *            {@code String}, {@code Map}, etc.
     * @return The new patch operation.
     */
    public static PatchOperation operation(final String operation, final String field, final Object value) {
        return operation(operation, new JsonPointer(field), value);
    }

    /**
     * Returns a deep copy of the provided patch operation. This method may be
     * used in cases where the immutability of the underlying JSON value cannot
     * be guaranteed.
     *
     * @param operation
     *            The patch operation to be defensively copied.
     * @return A deep copy of the provided patch operation.
     */
    public static PatchOperation copyOf(final PatchOperation operation) {
        return new PatchOperation(
            operation.getOperation(),
            operation.getField(),
            operation.getFrom(),
            operation.getValue().copy(),
            operation.toJsonValue().copy());
    }

    /**
     * Parses the provided JSON content as a patch operation.
     *
     * @param json
     *            The patch operation to be parsed.
     * @return The parsed patch operation.
     * @throws BadRequestException
     *             If the JSON value is not a JSON patch operation.
     */
    public static PatchOperation valueOf(final JsonValue json) throws BadRequestException {
        if (!json.isMap()) {
            throw new BadRequestException(
                        "The request could not be processed because the provided "
                                + "content is not a valid JSON patch");
        }
        try {
            return new PatchOperation(json.get(FIELD_OPERATION).asString(), json.get(FIELD_FIELD).as(pointer()),
                    json.get(FIELD_FROM).as(pointer()), json.get(FIELD_VALUE), json);
        } catch (final Exception e) {
            throw new BadRequestException(
                    "The request could not be processed because the provided "
                            + "content is not a valid JSON patch: " + e.getMessage(), e);
        }
    }

    /**
     * Parses the provided JSON content as a list of patch operations.
     *
     * @param json
     *            The list of patch operations to be parsed.
     * @return The list of parsed patch operations.
     * @throws BadRequestException
     *             If the JSON value is not a list of JSON patch operations.
     */
    public static List<PatchOperation> valueOfList(final JsonValue json) throws BadRequestException {
        if (!json.isList()) {
            throw new BadRequestException(
                    "The request could not be processed because the provided "
                            + "content is not a JSON array of patch operations");
        }
        final List<PatchOperation> patch = new ArrayList<>(json.size());
        for (final JsonValue operation : json) {
            patch.add(valueOf(operation));
        }
        return patch;
    }

    private final JsonPointer field;
    private final JsonPointer from;
    private final String operation;
    private final JsonValue value;
    private JsonValue json;

    private PatchOperation(final String operation, final JsonPointer field, final JsonPointer from,
                           final JsonValue value, final JsonValue json) {
        checkNotNull(operation, "Cannot instantiate PatchOperation with null 'operation' value");
        checkNotNull(field, "Cannot instantiate PatchOperation with null 'field' value");
        checkNotNull(value, "Cannot instantiate PatchOperation with null 'value' value");

        this.operation = operation;
        checkOperationType();
        this.field = field;
        this.value = value;
        this.from = from;
        this.json = json;

        if (isAdd() || isIncrement() || isReplace() || isTransform()) {
            if (value.isNull()) {
                throw new NullPointerException("No value field provided for '" + operation + "' operation");
            }
            if (from != null) {
                throw new IllegalArgumentException("'" + operation + "' does not accept from field");
            }
            if (isIncrement() && !value.isNumber()) {
                throw new IllegalArgumentException("Non-numeric value provided for increment operation");
            }
        } else if (isRemove()) {
            if (from != null) {
                throw new IllegalArgumentException("'" + operation + "' does not accept from field");
            }
        } else if (isCopy() || isMove()) {
            if (from == null || from.isEmpty()) {
                throw new NullPointerException("No from field provided for '" + operation + "' operation");
            }
            if (value.isNotNull()) {
                throw new IllegalArgumentException("'" + operation + "' does not accept value field");
            }
        }
    }

    private void checkOperationType() {
        if (!isAdd() && !isRemove() && !isIncrement() && !isReplace() && !isTransform() && !isMove() && !isCopy()) {
            throw new IllegalArgumentException("Invalid patch operation type " + operation);
        }
    }

    /**
     * Returns the field targeted by the patch operation.
     *
     * @return The field targeted by the patch operation.
     */
    public JsonPointer getField() {
        return field;
    }

    /**
     * Returns the source field for move and copy operations.
     *
     * @return The source field for move and copy operations.
     */
    public JsonPointer getFrom() {
        return from;
    }

    /**
     * Returns the type of patch operation to be performed.
     *
     * @return The type of patch operation to be performed.
     */
    public String getOperation() {
        return operation;
    }

    /**
     * Returns the value for the patch operation. The return value may be
     * a JSON value whose value is {@code null}.
     *
     * @return The nullable value for the patch operation.
     */
    public JsonValue getValue() {
        return value;
    }

    /**
     * Returns {@code true} if this is an "add" patch operation.
     *
     * @return {@code true} if this is an "add" patch operation.
     */
    public boolean isAdd() {
        return is(OPERATION_ADD);
    }

    /**
     * Returns {@code true} if this is an "increment" patch operation.
     *
     * @return {@code true} if this is an "increment" patch operation.
     */
    public boolean isIncrement() {
        return is(OPERATION_INCREMENT);
    }

    /**
     * Returns {@code true} if this is an "remove" patch operation.
     *
     * @return {@code true} if this is an "remove" patch operation.
     */
    public boolean isRemove() {
        return is(OPERATION_REMOVE);
    }

    /**
     * Returns {@code true} if this is an "replace" patch operation.
     *
     * @return {@code true} if this is an "replace" patch operation.
     */
    public boolean isReplace() {
        return is(OPERATION_REPLACE);
    }

    /**
     * Returns {@code true} if this is a "move" patch operation.
     *
     * @return {@code true} if this is a "move" patch operation.
     */
    public boolean isMove() {
        return is(OPERATION_MOVE);
    }

    /**
     * Returns {@code true} if this is a "copy" patch operation.
     *
     * @return {@code true} if this is a "copy" patch operation.
     */
    public boolean isCopy() {
        return is(OPERATION_COPY);
    }

    /**
     * Returns {@code true} if this is a "transform" patch operation.
     *
     * @return {@code true} if this is a "transform" patch operation.
     */
    public boolean isTransform() {
        return is(OPERATION_TRANSFORM);
    }

    /**
     * Returns a JSON value representation of this patch operation.
     *
     * @return A JSON value representation of this patch operation.
     */
    public JsonValue toJsonValue() {
        if (json == null) {
            json = new JsonValue(new LinkedHashMap<>());
            json.put(FIELD_OPERATION, operation);
            json.put(FIELD_FIELD, field.toString());
            if (from != null) {
                json.put(FIELD_FROM, from.toString());
            }
            if (value.isNotNull()) {
                json.put(FIELD_VALUE, value.getObject());
            }
        }
        return json;
    }

    @Override
    public String toString() {
        return toJsonValue().toString();
    }

    private boolean is(final String type) {
        return operation.equalsIgnoreCase(type);
    }
}