
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);
}
}
}