All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.obvj.jsonmerge.JsonMergeOption Maven / Gradle / Ivy

/*
 * Copyright 2022 obvj.net
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.obvj.jsonmerge;

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
import static net.obvj.jsonmerge.util.StringUtils.requireNonBlankAndTrim;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import com.jayway.jsonpath.InvalidPathException;

import net.obvj.jsonmerge.util.JsonPathExpression;

/**
 * An object that contains parameters about how to merge a specific path of a JSON
 * document.
 * 

* Use {@link JsonMergeOption#onPath(String)} to initialize the construction of a * {@code JsonMergeOption} with a specific document path, then let the API guide you * through the additional builder methods to finalize the construction of the desired * merge option. *

* Examples: * *

* *
 * {@code JsonMergeOption.onPath("$.parameters")}
 * {@code         .findObjectsIdentifiedBy("name")}
 * {@code         .thenPickTheHigherPrecedenceOne();}
 *
 * {@code JsonMergeOption.onPath("$.files")}
 * {@code         .findObjectsIdentifiedBy("id", "version")}
 * {@code         .thenDoADeepMerge();}
 *
 * {@code JsonMergeOption.onPath("$.files")}
 * {@code         .addDistinctObjectsOnly();}
 *
 * {@code JsonMergeOption.onPath("$.files")}
 * {@code         .addAll();}
 * 
* *
* * @author oswaldo.bapvic.jr * @since 1.0.0 */ public final class JsonMergeOption { private static final String TO_STRING_FORMAT = "JsonMergeOption (path=%s, keys=%s, deepMerge=%s, distinctObjectsOnly=%s)"; private static final boolean DEFAULT_DEEP_MERGE = false; private static final boolean DEFAULT_DISTINCT_OBJECTS_ONLY = true; /** * The default merge option. * * @since 1.1.0 */ public static final JsonMergeOption DEFAULT = new JsonMergeOption(new JsonPathExpression("@"), emptyList(), DEFAULT_DEEP_MERGE, DEFAULT_DISTINCT_OBJECTS_ONLY); private final JsonPathExpression path; private final List keys; private final boolean deepMerge; private final boolean distinctObjectsOnly; private JsonMergeOption(JsonPathExpression path, List keys, boolean deepMerge, boolean distinctObjectsOnly) { this.path = path; this.keys = unmodifiableList(keys); this.deepMerge = deepMerge; this.distinctObjectsOnly = distinctObjectsOnly; } /** * Specifies a distinct key for objects inside an array identified by a given * {@code JsonPath} expression. *

* For example, consider the following document:

* *
     * {
     *   "params": [
     *     {
     *       "param": "name",
     *       "value": "John Doe"
     *     },
     *     {
     *       "param": "age",
     *       "value": 33
     *     }
     *   ]
     * }
     * 
* *
* *

* A {@code MergeOption} of key {@code "param"} associated with the {@code JsonPath} * expression {@code "$.params"} tells the algorithm to check for distinct objects * identified by the same value inside the {@code "param"} field during the merge of the * {@code "$.params"} array. *

* In other words, if two JSON documents contain different objects identified by the same * key inside that array, then only the object from the highest-precedence JSON document * will be selected. *

* A {@code JsonPath} expression can be specified using either dot- or bracket-notation, * but complex expressions containing filters, script, subscript, or union operations, are * not supported. * * @param jsonPath a {@code JsonPath} expression that identifies the array; not empty * @param key the key to be considered unique for objects inside an array * * @return a new {@code JsonMergeOption} with the specified pair of distinct path and key * * @throws IllegalArgumentException if one of the parameters is null or blank * @throws InvalidPathException if the specified JsonPath expression is invalid * @deprecated Use {@link JsonMergeOption#onPath(String)} instead */ @Deprecated public static JsonMergeOption distinctKey(String jsonPath, String key) { return distinctKeys(jsonPath, key); } /** * Specifies one or more distinct keys for objects inside an array identified by a given * {@code JsonPath} expression. *

* For example, consider the following document:

* *
     * {
     *   "files": [
     *     {
     *       "id": "d2b638be-40d2-4965-906e-291521f8a19d",
     *       "version": "1",
     *       "date": "2022-07-07T10:42:21"
     *     },
     *     {
     *       "id": "d2b638be-40d2-4965-906e-291521f8a19d",
     *       "version": "2",
     *       "date": "2022-08-06T09:40:01"
     *     },
     *     {
     *       "id": "9570cc646-1586-11ed-861d-0242ac120002",
     *       "version": "1",
     *       "date": "2022-08-06T09:51:40"
     *     }
     *   ]
     * }
     * 
* *
* *

* A {@code MergeOption} of keys {@code "id"} and {@code "version"} associated with the * {@code JsonPath} expression {@code "$.files"} tells the algorithm to check for distinct * objects identified by the same values in both fields {@code "id"} and {@code "version"} * during the merge of the {@code "$.files"} array. *

* In other words, if two JSON documents contain different objects identified by the same * keys inside that array, then only the object from the highest-precedence JSON document * will be selected. *

* A {@code JsonPath} expression can be specified using either dot- or bracket-notation, * but complex expressions containing filters, script, subscript, or union operations, are * not supported. * * @param jsonPath a {@code JsonPath} expression that identifies the array; not empty * @param keys one or more keys to be considered unique for objects inside an array * * @return a new {@link JsonMergeOption} with the specified pair of path and distinct keys * * @throws IllegalArgumentException if one of the parameters is null or blank * @throws InvalidPathException if the specified JsonPath expression is invalid * @deprecated Use {@link JsonMergeOption#onPath(String)} instead */ @Deprecated public static JsonMergeOption distinctKeys(String jsonPath, String... keys) { return distinctKeys(new JsonPathExpression(jsonPath), keys); } /** * Specifies one or more distinct keys for objects inside an array identified by a given * {@code JsonPathExpression}. * * @param path a {@link JsonPathExpression} that identifies the array; not empty * @param keys one or more keys to be considered unique for objects inside an array * * @return a new {@link JsonMergeOption} with the specified pair of distinct key and path * * @throws IllegalArgumentException if one of the specified parameters is null or blank * @throws InvalidPathException if the specified JsonPath expression is invalid * @deprecated Use {@link JsonMergeOption#onPath(String)} instead */ @Deprecated private static JsonMergeOption distinctKeys(JsonPathExpression path, String... keys) { List trimmedKeys = Arrays.stream(keys) .map(key -> requireNonBlankAndTrim(key, "The key must not be null or blank")) .collect(toList()); return new JsonMergeOption(path, trimmedKeys, DEFAULT_DEEP_MERGE, DEFAULT_DISTINCT_OBJECTS_ONLY); } /** * Initializes the construction of a {@code JsonMergeOption} for the document path * determined by the given {@code JsonPath} expression. *

* A {@code JsonPath} expression can be specified using either dot- or bracket-notation, * but complex expressions containing filters, script, subscript, or union operations, are * not fully supported. *

* Examples of valid expressions: *

    *
  • Using dot-notation * *
         * $.sites
         * $.sites[*].users
         * $.sites[*].users[*].preferences
         * 
    * *
  • *
  • Using bracket-notation * *
         * $['sites']
         * $['sites'][*]['users']
         * $['sites'][*]['users'][*]['preferences']
         * 
    * *
  • *
* Sample JSON: * *
     * {
     *   "sites": [
     *     {
     *       "id": "europe-1",
     *       "users": [
     *         {
     *           "email": "[email protected]",
     *           "preferences": [
     *             {
     *               "key": "theme",
     *               "value": "light"
     *             }
     *           ]
     *         }
     *       ]
     *     }
     *   ]
     * }
     * 
* * @param jsonPath a {@code JsonPath} expression that identifies the document part that * should receive special handling during the merge; not empty * @return a {@code JsonMergeOption} builder initialized with the specified * {@code JsonPath} * * @throws IllegalArgumentException if the specified expression is null or empty * @throws InvalidPathException if the specified {@code JsonPath} expression is * invalid * @since 1.1.0 * @see JsonPathExpression */ public static Builder onPath(String jsonPath) { JsonPathExpression compiledJsonPath = new JsonPathExpression(jsonPath); return new Builder(compiledJsonPath); } /** * Returns a {@code JsonPath} expression that represents a specific path of the JSON * document to receive special handling during the merge * * @return a {@link JsonPathExpression}; not null * @since 1.1.0 */ public JsonPathExpression getPath() { return path; } /** * Returns a list of keys to be considered for distinct JSON objects identification inside * the array represented by {@link #getPath()}. * * @return a list containing keys, or an empty list; never null * @since 1.1.0 */ public List getKeys() { return keys; } /** * Returns a flag indicating whether or not to do a deep merge of the elements inside the * document path represented by {@link #getPath()}. * * @return a flag indicating whether or not to do a deep merge of the path * @since 1.1.0 */ public boolean isDeepMerge() { return deepMerge; } /** * Returns a flag indicating whether or not to check for duplicate objects before adding * them during the merge of the array path represented by {@link #getPath()}. * * @return a flag indicating whether or not to avoid duplicate objects during the merge * @since 1.1.0 */ public boolean isDistinctObjectsOnly() { return distinctObjectsOnly; } /** * Returns a string representation of this {@code JsonMergeOption}. * * @return a string representation of this object * @since 1.1.0 */ @Override public String toString() { return String.format(TO_STRING_FORMAT, path, keys, deepMerge, distinctObjectsOnly); } /** * Returns a hash code value for the object. * * @since 1.2.0 */ @Override public int hashCode() { return Objects.hash(path); } /** * Indicates whether some other object is "equal to" this one. *

* Two {@code JsonMergeOption}s will be considered equal if they share the same path. * * @since 1.2.0 */ @Override public boolean equals(Object object) { if (this == object) return true; if (object == null) return false; if (getClass() != object.getClass()) return false; JsonMergeOption other = (JsonMergeOption) object; return Objects.equals(path, other.path); } /** * A builder for {@link JsonMergeOption}, with the document path already set. * * @author oswaldo.bapvic.jr * @since 1.1.0 */ public static class Builder { private final JsonPathExpression path; private Builder(JsonPathExpression path) { this.path = path; } /** * Defines one or more keys to determine object equality inside the specified document * path, provided that the path is an array path. *

* For example, consider the following JSON document:

* *
         * {
         *   "params": [
         *     {
         *       "name": "country",
         *       "value": "Brazil"
         *     },
         *     {
         *       "name": "language",
         *       "value": "pt-BR"
         *     }
         *   ]
         * }
         * 
* *
*

* A {@code JsonMergeOption} associating the {@code "name"} key with the * {@code "$.params"} path tells the algorithm to find distinct objects identified by the * {@code "name"} key during the merge of the {@code "$.params"} array. *

* In other words, if two JSON documents contain different objects with same value for the * provided key(s), then they will be considered as "equal" during the merge. * * @param keys one or more keys to determine object equality inside an array path * @return an intermediary {@code JsonMergeOption} build stage with the specified key(s) * @throws IllegalArgumentException if a {@code null} or blank key is received */ public BuilderWithKeys findObjectsIdentifiedBy(String... keys) { List trimmedKeys = Arrays.stream(keys) .map(key -> requireNonBlankAndTrim(key, "The key must not be null or blank")) .collect(toList()); return new BuilderWithKeys(path, trimmedKeys); } /** * Avoid duplicate objects during the merge of the specified array path (default option). * * @return a finalized {@link JsonMergeOption} */ public JsonMergeOption addDistinctObjectsOnly() { return new JsonMergeOption(path, emptyList(), DEFAULT_DEEP_MERGE, true); } /** * Add all elements of the both JSON documents during the merge of the specified array * path, with no duplication check. * * @return a finalized {@link JsonMergeOption} */ public JsonMergeOption addAll() { return new JsonMergeOption(path, emptyList(), DEFAULT_DEEP_MERGE, false); } } /** * An intermediary {@link JsonMergeOption} build stage that is applicable when one or more * distinct keys are specified for a given document path. * * @author oswaldo.bapvic.jr * @since 1.1.0 */ public static class BuilderWithKeys { private final JsonPathExpression path; private final List keys; private BuilderWithKeys(JsonPathExpression path, List keys) { this.path = path; this.keys = keys; } /** * Tells the algorithm to pick the highest precedence object when two objects identified * by the same key(s) are found in both JSON documents at the path defined for the * {@code JsonMergeOption}. * * @return a finalized {@link JsonMergeOption} */ public JsonMergeOption thenPickTheHighestPrecedenceOne() { return new JsonMergeOption(path, keys, false, DEFAULT_DISTINCT_OBJECTS_ONLY); } /** * Tells the algorithm to do a deep merge if two objects identified by the same key(s) are * found in both JSON documents at the path defined for the {@code JsonMergeOption}. * * @return a finalized {@link JsonMergeOption} */ public JsonMergeOption thenDoADeepMerge() { return new JsonMergeOption(path, keys, true, DEFAULT_DISTINCT_OBJECTS_ONLY); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy