ConflictResolutionPolicy.java

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

package com.azure.cosmos.models;


import com.azure.cosmos.implementation.Constants;
import com.azure.cosmos.implementation.JsonSerializable;
import com.azure.cosmos.implementation.Paths;
import com.azure.cosmos.implementation.Resource;
import com.azure.cosmos.implementation.StoredProcedure;
import com.azure.cosmos.implementation.Strings;
import com.fasterxml.jackson.databind.node.ObjectNode;


/**
 * Represents the conflict resolution policy configuration for specifying how to resolve conflicts
 * in case writes from different regions result in conflicts on items in the container in the Azure Cosmos DB
 * service.
 *
 * Refer to: https://docs.microsoft.com/en-us/azure/cosmos-db/conflict-resolution-policies
 *
 * <p>
 * A container with custom conflict resolution with no user-registered stored procedure.
 * <pre>{@code
 *
 * CosmosContainerProperties containerProperties =
 *      new CosmosContainerProperties("Multi-master container", "Multi-master container partition key");
 * containerProperties.setConflictResolutionPolicy(ConflictResolutionPolicy.createCustomPolicy());
 *
 * CosmosAsyncDatabase database = client.createDatabase(databaseProperties).block().getDatabase();
 * CosmosAsyncContainer container = database.createContainer(containerProperties).block().getContainer();
 *
 * }
 * </pre>
 * <p>
 * A container with custom conflict resolution with a user-registered stored procedure.
 * <pre>{@code
 *
 * CosmosContainerProperties containerProperties =
 *      new CosmosContainerProperties("Multi-master container", "Multi-master container partition key");
 *
 * ConflictResolutionPolicy policy = ConflictResolutionPolicy.createCustomPolicy(conflictResolutionSprocName);
 * containerProperties.setConflictResolutionPolicy(policy);
 *
 * CosmosAsyncDatabase database = client.createDatabase(databaseProperties).block().getDatabase();
 * CosmosAsyncContainer container = database.createContainer(containerProperties).block().getContainer();
 *
 * }
 * </pre>
 * <p>
 * A container with last writer wins conflict resolution, based on a path in the conflicting items.
 * A container with custom conflict resolution with a user-registered stored procedure.
 * <pre>{@code
 *
 * CosmosContainerProperties containerProperties =
 *      new CosmosContainerProperties("Multi-master container", "Multi-master container partition key");
 *
 * ConflictResolutionPolicy policy = ConflictResolutionPolicy.createLastWriterWinsPolicy("/path/for/conflict/resolution");
 * containerProperties.setConflictResolutionPolicy(policy);
 *
 * CosmosAsyncDatabase database = client.createDatabase(databaseProperties).block().getDatabase();
 * CosmosAsyncContainer container = database.createContainer(containerProperties).block().getContainer();
 *
 * }
 * </pre>
 */
public final class ConflictResolutionPolicy {

    private JsonSerializable jsonSerializable;

    /**
     * Creates a LAST_WRITER_WINS {@link ConflictResolutionPolicy} with "/_ts" as the resolution path.
     * <p>
     * In case of a conflict occurring on an item, the item with the higher integer value in the default path
     * {@link Resource#getTimestamp()} ()}, i.e., "/_ts" will be used.
     * {@link Resource#getTimestamp()}, i.e., "/_ts" will be used.
     *
     * @return ConflictResolutionPolicy.
     */
    public static ConflictResolutionPolicy createLastWriterWinsPolicy() {
        ConflictResolutionPolicy policy = new ConflictResolutionPolicy();
        policy.setMode(ConflictResolutionMode.LAST_WRITER_WINS);
        return policy;
    }

    /**
     * Creates a LAST_WRITER_WINS {@link ConflictResolutionPolicy} with path as the resolution path.
     * <p>
     * The specified path must be present in each item and must be an integer value.
     * In case of a conflict occurring on an item, the item with the higher integer value in the specified path
     * will be picked.
     *
     * @param conflictResolutionPath The path to check values for last-writer wins conflict resolution.
     * That path is a rooted path of the property in the item, such as "/name/first".
     * @return ConflictResolutionPolicy.
     */
    public static ConflictResolutionPolicy createLastWriterWinsPolicy(String conflictResolutionPath) {
        ConflictResolutionPolicy policy = new ConflictResolutionPolicy();
        policy.setMode(ConflictResolutionMode.LAST_WRITER_WINS);
        if (conflictResolutionPath != null) {
            policy.setConflictResolutionPath(conflictResolutionPath);
        }
        return policy;
    }

    /**
     * Creates a CUSTOM {@link ConflictResolutionPolicy} which uses the specified stored procedure
     * to perform conflict resolution
     * <p>
     * This stored procedure may be created after the {@link CosmosContainerProperties} is created and can be changed as
     * required.
     *
     * <ul>
     * <li>This method requires conflictResolutionStoredProcFullPath in format
     * dbs/%s/colls/%s/sprocs/%s. User can also use equivalent method {@link #createCustomPolicy(String, String, String)}</li>
     * <li>In case the stored procedure fails or throws an exception,
     * the conflict resolution will default to registering conflicts in the conflicts feed</li>
     * <li>The user can provide the stored procedure @see {@link Resource#getId()} </li>
     * </ul>
     *
     * @param conflictResolutionStoredProcFullPath stored procedure full path to perform conflict resolution.
     * @return ConflictResolutionPolicy.
     */
    public static ConflictResolutionPolicy createCustomPolicy(String conflictResolutionStoredProcFullPath) {
        ConflictResolutionPolicy policy = new ConflictResolutionPolicy();
        policy.setMode(ConflictResolutionMode.CUSTOM);
        if (conflictResolutionStoredProcFullPath != null) {
            policy.setConflictResolutionProcedure(conflictResolutionStoredProcFullPath);
        }
        return policy;
    }

    /**
     * Creates a CUSTOM {@link ConflictResolutionPolicy} which uses the specified stored procedure
     * to perform conflict resolution
     * <p>
     * This stored procedure may be created after the {@link CosmosContainerProperties} is created and can be changed as
     * required.
     *
     * <ul>
     * <li>In case the stored procedure fails or throws an exception,
     * the conflict resolution will default to registering conflicts in the conflicts feed</li>
     * <li>The user can provide the stored procedure @see {@link Resource#getId()} </li>
     * </ul>
     *
     * @param dbName database name.
     * @param containerName container name.
     * @param sprocName stored procedure name to perform conflict resolution.
     * @return ConflictResolutionPolicy.
     */
    public static ConflictResolutionPolicy createCustomPolicy(String dbName, String containerName, String sprocName) {
        return createCustomPolicy(getFullPath(dbName, containerName, sprocName));
    }

    /**
     * Creates a CUSTOM {@link ConflictResolutionPolicy} without any {@link StoredProcedure}. User manually
     * should resolve conflicts.
     * <p>
     * The conflicts will be registered in the conflicts feed and the user should manually resolve them.
     *
     * @return ConflictResolutionPolicy.
     */
    public static ConflictResolutionPolicy createCustomPolicy() {
        ConflictResolutionPolicy policy = new ConflictResolutionPolicy();
        policy.setMode(ConflictResolutionMode.CUSTOM);
        return policy;
    }

    /**
     * Initializes a new instance of the {@link ConflictResolutionPolicy} class for the Azure Cosmos DB service.
     */
    ConflictResolutionPolicy() {
        this.jsonSerializable = new JsonSerializable();
    }

    /**
     * Instantiates a new Conflict resolution policy.
     *
     * @param jsonString the json string
     */
    ConflictResolutionPolicy(String jsonString) {
        this.jsonSerializable = new JsonSerializable(jsonString);
    }

    /**
     * Instantiates a new Conflict resolution policy.
     *
     * @param objectNode the object node.
     */
    ConflictResolutionPolicy(ObjectNode objectNode) {
        this.jsonSerializable = new JsonSerializable(objectNode);
    }

    /**
     * Gets the {@link ConflictResolutionMode} in the Azure Cosmos DB service.
     * By default it is {@link ConflictResolutionMode#LAST_WRITER_WINS}.
     *
     * @return ConflictResolutionMode.
     */
    public ConflictResolutionMode getMode() {

        String strValue = this.jsonSerializable.getString(Constants.Properties.MODE);

        if (!Strings.isNullOrEmpty(strValue)) {
            try {
                return ConflictResolutionMode
                           .valueOf(Strings.fromCamelCaseToUpperCase(this.jsonSerializable.getString(Constants.Properties.MODE)));
            } catch (IllegalArgumentException e) {
                this.jsonSerializable.getLogger().warn("INVALID ConflictResolutionMode getValue {}.",
                    this.jsonSerializable.getString(Constants.Properties.MODE));
                return ConflictResolutionMode.INVALID;
            }
        }

        return ConflictResolutionMode.INVALID;
    }

    /**
     * Sets the {@link ConflictResolutionMode} in the Azure Cosmos DB service.
     * By default it is {@link ConflictResolutionMode#LAST_WRITER_WINS}.
     *
     * @param mode One of the values of the {@link ConflictResolutionMode} enum.
     */
    ConflictResolutionPolicy setMode(ConflictResolutionMode mode) {
        this.jsonSerializable.set(Constants.Properties.MODE, mode.toString());
        return this;
    }

    /**
     * Gets the path which is present in each item in the Azure Cosmos DB service for last writer wins
     * conflict-resolution.
     * This path must be present in each item and must be an integer value.
     * In case of a conflict occurring on an item, the item with the higher integer value in the specified
     * path will be picked.
     * If the path is unspecified, by default the {@link Resource#getTimestamp()} ()} path will be used.
     * <p>
     * This value should only be set when using {@link ConflictResolutionMode#LAST_WRITER_WINS}
     *
     * @return The path to check values for last-writer wins conflict resolution.
     * That path is a rooted path of the property in the item, such as "/name/first".
     */
    public String getConflictResolutionPath() {
        return this.jsonSerializable.getString(Constants.Properties.CONFLICT_RESOLUTION_PATH);
    }

    /**
     * Sets the path which is present in each item in the Azure Cosmos DB service for last writer wins
     * conflict-resolution.
     * This path must be present in each item and must be an integer value.
     * In case of a conflict occurring on an item, the item with the higher integer value in the specified
     * path will be picked.
     * If the path is unspecified, by default the {@link Resource#getTimestamp()} ()} path will be used.
     * <p>
     * This value should only be set when using {@link ConflictResolutionMode#LAST_WRITER_WINS}
     *
     * @param value The path to check values for last-writer wins conflict resolution.
     * That path is a rooted path of the property in the item, such as "/name/first".
     */
    ConflictResolutionPolicy setConflictResolutionPath(String value) {
        this.jsonSerializable.set(Constants.Properties.CONFLICT_RESOLUTION_PATH, value);
        return this;
    }

    /**
     * Gets the {@link StoredProcedure} which is used for conflict resolution in the Azure Cosmos DB service.
     * This stored procedure may be created after the {@link CosmosContainerProperties} is created and can be changed as
     * required.
     *
     * <ul>
     * <li>This value should only be set when using {@link ConflictResolutionMode#CUSTOM}</li>
     * <li>In case the stored procedure fails or throws an exception,
     * the conflict resolution will default to registering conflicts in the conflicts feed</li>
     * <li>The user can provide the stored procedure @see {@link Resource#getId()} ()}</li>
     * </ul>
     * *
     *
     * @return the stored procedure to perform conflict resolution.]
     */
    public String getConflictResolutionProcedure() {
        return this.jsonSerializable.getString(Constants.Properties.CONFLICT_RESOLUTION_PROCEDURE);
    }

    ConflictResolutionPolicy setConflictResolutionProcedure(String value) {
        this.jsonSerializable.set(Constants.Properties.CONFLICT_RESOLUTION_PROCEDURE, value);
        return this;
    }

    void populatePropertyBag() {
        this.jsonSerializable.populatePropertyBag();
    }

    JsonSerializable getJsonSerializable() { return this.jsonSerializable; }

    private static String getFullPath(String dbName, String containerName, String sprocName) {
        if (dbName == null) {
            throw new IllegalArgumentException("dbName cannot be null");
        }

        if (containerName == null) {
            throw new IllegalArgumentException("containerName cannot be null");
        }

        if (sprocName == null) {
            throw new IllegalArgumentException("sprocName cannot be null");
        }

        StringBuilder builder = new StringBuilder();
        builder.append(Paths.DATABASES_PATH_SEGMENT);
        builder.append("/");
        builder.append(dbName);
        builder.append("/");
        builder.append(Paths.COLLECTIONS_PATH_SEGMENT);
        builder.append("/");
        builder.append(containerName);
        builder.append("/");
        builder.append(Paths.STORED_PROCEDURES_PATH_SEGMENT);
        builder.append("/");
        builder.append(sprocName);
        return builder.toString();
    }
}