CosmosPatchOperations.java

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.models;

import com.azure.cosmos.implementation.ImplementationBridgeHelpers;
import com.azure.cosmos.implementation.apachecommons.lang.StringUtils;
import com.azure.cosmos.implementation.patch.PatchOperation;
import com.azure.cosmos.implementation.patch.PatchOperationCore;
import com.azure.cosmos.implementation.patch.PatchOperationType;

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

import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkArgument;
import static com.azure.cosmos.implementation.guava25.base.Preconditions.checkNotNull;

/**
 * Grammar is a super set of this RFC: https://tools.ietf.org/html/rfc6902#section-4.1
 *
 * Contains a list of Patch operations to be applied on an item. It is applied in an atomic manner and we support all
 * the operation in above RFC and more.
 *
 * This can be executed in 3 ways:
 *  1. Passing this to container in container.patchItem() which requires the id of the item to be patched, partition
 *      key, the CosmosPatchOperations instance, any CosmosItemRequestOptions and the class type for which response will be parsed.
 *  2. Add CosmosPatchOperations instance in TransactionalBatch using batch.patchItemOperation() which requires the id of the item
 *      to be patched, cosmos patch instance and TransactionalBatchItemRequestOptions(if-any) and follow remaining
 *      steps for batch for it's execution.
 *  3. Create a bulk item using {@link CosmosBulkOperations#getPatchItemOperation(String, PartitionKey, CosmosPatchOperations)} which requires the id of the item to be patched,
 *      cosmos patch instance, partition key and {@link CosmosBulkItemRequestOptions}(if-any) and follow remaining steps to
 *      execute bulk operations.
 *
 *  Let's assume this is the JSON for which we want to run patch operation.
 *  <code>
 *      {
 *          a : "xyz"
 *          b : {
 *              c : "efg:
 *              d : 4
 *              e : [0, 1, 2 , 3]
 *          }
 *      }
 *  </code>
 *
 */
public final class CosmosPatchOperations {

    private final List<PatchOperation> patchOperations;

    private CosmosPatchOperations() {
        this.patchOperations = new ArrayList<>();
    }

    /**
     * Initializes a new instance of {@link CosmosPatchOperations} that will contain operations to be performed on a item atomically.
     *
     * @return A new instance of {@link CosmosPatchOperations}.
     */
    public static CosmosPatchOperations create() {
        return new CosmosPatchOperations();
    }

    /**
     * This performs one of the following functions, depending upon what the target location references:
     *  1. Target location specifies an array index, a new value is inserted into the array at the specified index.
     *  2. Target location specifies an object member that does not already exist, a new member is added to the object.
     *  3. Target location specifies an object member that does exist, that member's value is replaced.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.add("/b/e", 15); // will add a value to the array, so /b/e array will become [0, 1, 2, 3, 15]
     *     cosmosPatch.add("/a", "new value"); // will replace the value
     *     cosmosPatch.add("/b/e/1", 10); // will change value of the /b/e array to [0, 10, 2, 3]
     * </code>
     *
     * This operation is not idempotent for scenario 1 and 2. For 3rd it is as the final value will be the value
     * provided here.
     *
     * @param <T> The type of item to be added.
     *
     * @param path the operation path.
     * @param value the value which will be added.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public <T> CosmosPatchOperations add(String path, T value) {

        checkNotNull(value, "expected non-null value");
        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.ADD,
                path,
                value));

        return this;
    }

    /**
     * This removes the value at the target location.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.remove("/a");
     *     cosmosPatch.remove("/b/e/3"); // will remove 4th element of /b/e array
     * </code>
     *
     * This operation is not idempotent. Since once applied, next time it will return bad request due to path not found.
     *
     * @param path the operation path.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public CosmosPatchOperations remove(String path) {

        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.REMOVE,
                path,
                null));

        return this;
    }

    /**
     * This replaces the value at the target location with a new value.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.replace("/a", "new value"); // will replace "xyz" to "new value"
     *     cosmosPatch.replace("/b/e/1", 2); // will replace 2nd element of /b/e array to 2
     * </code>
     *
     * This operation is idempotent as multiple call execution replace to the same value.
     *
     * @param <T> The type of item to be replaced.
     *
     * @param path the operation path.
     * @param value the value which will be replaced.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public <T> CosmosPatchOperations replace(String path, T value) {

        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.REPLACE,
                path,
                value));

        return this;
    }

    /**
     * This sets the value at the target location with a new value.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.set("/f", "new value"); // will add a new path "/f" and set it's value as "new value".
     *     cosmosPatch.set("/b/e", "bar"); // will set "/b/e" path to be "bar".
     * </code>
     *
     * This operation is idempotent as multiple execution will set the same value. If a new path is added, next time
     * same value will be set.
     *
     * @param <T> The type of item to be set.
     *
     * @param path the operation path.
     * @param value the value which will be set.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public <T> CosmosPatchOperations set(String path, T value) {

        checkNotNull(value, "expected non-null value");
        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.SET,
                path,
                value));

        return this;
    }

    /**
     * This increment the value at the target location. It's a CRDT operator and won't cause any conflict.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.increment("/b/d", 1); // will add 1 to "/b/d" resulting in 5.
     * </code>
     *
     * This is not idempotent as multiple execution will increase the value by the given increment. For multi-region
     * we do support concurrent increment on different regions and the final value is a merged value combining
     * all increments value.
     * However if multiple increments are on the same region, it can lead to concurrency issue which can be retried.
     *
     * @param path the operation path.
     * @param value the value which will be incremented.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public CosmosPatchOperations increment(String path, long value) {

        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.INCREMENT,
                path,
                value));

        return this;
    }

    /**
     * This increment the value at the target location.
     *
     * For the above JSON, we can have something like this:
     * <code>
     *     CosmosPatchOperations cosmosPatch = CosmosPatchOperations.create();
     *     cosmosPatch.increment("/b/d", 3.5); // will add 3.5 to "/b/d" resulting in 7.5.
     * </code>
     *
     * This is not idempotent as multiple execution will increase the value by the given increment. For multi-region
     * we do support concurrent increment on different regions and the final value is a merged value combining
     * all increments values.
     * However if multiple increments are on the same region, it can lead to concurrency issue which can be retried.
     *
     * @param path the operation path.
     * @param value the value which will be incremented.
     *
     * @return same instance of {@link CosmosPatchOperations}
     */
    public CosmosPatchOperations increment(String path, double value) {

        checkArgument(StringUtils.isNotEmpty(path), "path empty %s", path);

        this.patchOperations.add(
            new PatchOperationCore<>(
                PatchOperationType.INCREMENT,
                path,
                value));

        return this;
    }

    List<PatchOperation> getPatchOperations() {
        return patchOperations;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////
    // the following helper/accessor only helps to access this class outside of this package.//
    ///////////////////////////////////////////////////////////////////////////////////////////

    static {
        ImplementationBridgeHelpers.CosmosPatchOperationsHelper.setCosmosPatchOperationsAccessor(
            new ImplementationBridgeHelpers.CosmosPatchOperationsHelper.CosmosPatchOperationsAccessor() {
                @Override
                public List<PatchOperation> getPatchOperations(CosmosPatchOperations cosmosPatchOperations) {
                    return cosmosPatchOperations.getPatchOperations();
                }
            }
        );
    }
}