JsonPatchDocument.java

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

package com.azure.core.models;

import com.azure.core.implementation.Option;
import com.azure.core.util.logging.ClientLogger;
import com.azure.core.util.serializer.JacksonAdapter;
import com.azure.core.util.serializer.JsonSerializer;
import com.azure.core.util.serializer.JsonSerializerProviders;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonValue;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Represents a JSON Patch document.
 */
public final class JsonPatchDocument {
    private static final Object SERIALIZER_INSTANTIATION_SYNCHRONIZER = new Object();
    private static volatile JsonSerializer defaultSerializer;

    @JsonIgnore
    private final ClientLogger logger = new ClientLogger(JsonPatchDocument.class);

    @JsonIgnore
    private final JsonSerializer serializer;

    @JsonValue
    private final List<JsonPatchOperation> operations;

    /**
     * Creates a new JSON Patch document.
     */
    public JsonPatchDocument() {
        this(null);
    }

    /**
     * Creates a new JSON Patch document.
     * <p>
     * If {@code serializer} isn't specified {@link JacksonAdapter} will be used.
     *
     * @param serializer The {@link JsonSerializer} that will be used to serialize patch operation values.
     */
    public JsonPatchDocument(JsonSerializer serializer) {
        this.operations = new ArrayList<>();
        this.serializer = serializer;
    }

    /**
     * Gets a representation of the {@link JsonPatchOperation JSON patch operations} in this JSON patch document.
     * <p>
     * Modifications to the returned list won't mutate the operations in the document.
     *
     * @return The JSON patch operations in this JSON patch document.
     */
    List<JsonPatchOperation> getOperations() {
        return new ArrayList<>(operations);
    }

    /**
     * Appends an "add" operation to this JSON Patch document.
     * <p>
     * If the {@code path} doesn't exist a new member is added to the object. If the {@code path} does exist the
     * previous value is replaced. If the {@code path} specifies an array index the value is inserted at the specified.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.1">JSON Patch Add</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendAdd#String-Object}
     *
     * @param path The path to apply the addition.
     * @param value The value that will be serialized and added to the path.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendAdd(String path, Object value) {
        return appendAddInternal(path, serializeValue(value));
    }

    /**
     * Appends an "add" operation to this JSON Patch document.
     * <p>
     * If the {@code path} doesn't exist a new member is added to the object. If the {@code path} does exist the
     * previous value is replaced. If the {@code path} specifies an array index the value is inserted at the specified.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.1">JSON Patch Add</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendAddRaw#String-String}
     *
     * @param path The path to apply the addition.
     * @param rawJson The raw JSON value that will be added to the path.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendAddRaw(String path, String rawJson) {
        return appendAddInternal(path, Option.of(rawJson));
    }

    private JsonPatchDocument appendAddInternal(String path, Option<String> rawJsonOption) {
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.ADD, null, path, rawJsonOption);
    }

    /**
     * Appends a "replace" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.3">JSON Patch replace</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendReplace#String-Object}
     *
     * @param path The path to replace.
     * @param value The value will be serialized and used as the replacement.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendReplace(String path, Object value) {
        return appendReplaceInternal(path, serializeValue(value));
    }

    /**
     * Appends a "replace" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.3">JSON Patch replace</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendReplaceRaw#String-String}
     *
     * @param path The path to replace.
     * @param rawJson The raw JSON value that will be used as the replacement.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendReplaceRaw(String path, String rawJson) {
        return appendReplaceInternal(path, Option.of(rawJson));
    }

    private JsonPatchDocument appendReplaceInternal(String path, Option<String> rawJsonOption) {
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.REPLACE, null, path, rawJsonOption);
    }

    /**
     * Appends a "copy" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.5">JSON Patch copy</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendCopy#String-String}
     *
     * @param from The path to copy from.
     * @param path The path to copy to.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code from} or {@code path} is null.
     */
    public JsonPatchDocument appendCopy(String from, String path) {
        Objects.requireNonNull(from, "'from' cannot be null.");
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.COPY, from, path, Option.uninitialized());
    }

    /**
     * Appends a "move" operation to this JSON Patch document.
     * <p>
     * For the operation to be successful {@code path} cannot be a child node of {@code from}.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.4">JSON Patch move</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendMove#String-String}
     *
     * @param from The path to move from.
     * @param path The path to move to.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code from} or {@code path} is null.
     */
    public JsonPatchDocument appendMove(String from, String path) {
        Objects.requireNonNull(from, "'from' cannot be null.");
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.MOVE, from, path, Option.uninitialized());
    }

    /**
     * Appends a "remove" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.2">JSON Patch remove</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendRemove#String}
     *
     * @param path The path to remove.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendRemove(String path) {
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.REMOVE, null, path, Option.uninitialized());
    }

    /**
     * Appends a "test" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.6">JSON Patch test</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendTest#String-Object}
     *
     * @param path The path to test.
     * @param value The value that will be serialized and used to test against.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendTest(String path, Object value) {
        return appendTestInternal(path, serializeValue(value));
    }

    /**
     * Appends a "test" operation to this JSON Patch document.
     * <p>
     * See <a href="https://tools.ietf.org/html/rfc6902#section-4.6">JSON Patch test</a> for more information.
     *
     * <p><strong>Code Samples</strong></p>
     *
     * {@codesnippet com.azure.core.util.JsonPatchDocument.appendTestRaw#String-String}
     *
     * @param path The path to test.
     * @param rawJson The raw JSON value that will be used to test against.
     * @return The updated JsonPatchDocument object.
     * @throws NullPointerException If {@code path} is null.
     */
    public JsonPatchDocument appendTestRaw(String path, String rawJson) {
        return appendTestInternal(path, Option.of(rawJson));
    }

    private JsonPatchDocument appendTestInternal(String path, Option<String> rawJsonOption) {
        Objects.requireNonNull(path, "'path' cannot be null.");

        return appendOperation(JsonPatchOperationKind.TEST, null, path, rawJsonOption);
    }

    private Option<String> serializeValue(Object value) {
        if (value == null) {
            return Option.empty();
        }

        String rawValue;
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            if (serializer == null) {
                if (defaultSerializer == null) {
                    synchronized (SERIALIZER_INSTANTIATION_SYNCHRONIZER) {
                        if (defaultSerializer == null) {
                            defaultSerializer = JsonSerializerProviders.createInstance();
                        }
                    }
                }

                defaultSerializer.serialize(outputStream, value);
            } else {
                serializer.serialize(outputStream, value);
            }

            rawValue = outputStream.toString("UTF-8");
        } catch (IOException ex) {
            throw logger.logExceptionAsError(new UncheckedIOException(ex));
        }

        return Option.of(rawValue);
    }

    private JsonPatchDocument appendOperation(JsonPatchOperationKind operationKind, String from, String path,
        Option<String> optionalValue) {
        operations.add(new JsonPatchOperation(operationKind, from, path, optionalValue));
        return this;
    }

    /**
     * Gets a formatted JSON string representation of this JSON Patch document.
     *
     * @return The formatted JSON String representing this JSON Patch docuemnt.
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");

        for (int i = 0; i < operations.size(); i++) {
            if (i > 0) {
                builder.append(",");
            }

            operations.get(i).buildString(builder);
        }

        return builder.append("]").toString();
    }
}