![JAR search and dependency download from the Maven repository](/logo.png)
personthecat.catlib.serialization.json.JsonTransformer Maven / Gradle / Ivy
package personthecat.catlib.serialization.json;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.Nullable;
import xjs.comments.CommentType;
import xjs.core.*;
import xjs.transform.JsonCollectors;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static personthecat.catlib.util.Shorthand.f;
/**
* This class contains a series of high level utilities to be used for updating old JSON presets
* to contain the current field names and syntax standards. It is designed to be used in a builder
* pattern and can handle renaming fields and collapsing nested objects into single objects.
*
* Renaming Values:
*
*
* For example, when given the following JSON data:
*
* {@code
* a: {
* b: [
* {
* old: value
* }
* {
* other: value
* }
* ]
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a", "b")
* .history("old", "other", "new")
* .updateAll(json);
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* b: [
* {
* new: value
* }
* {
* new: value
* }
* ]
* }
* }
* Marking Fields as Removed
*
* For another example, when given the following JSON data:
*
* {@code
* a: {
* container: {
* removed: value
* }
* }
* b: {
* container: {
* removed: value
* }
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.recursive("container")
* .markRemoved("removed", "1.0')
* .updateAll(json);
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* container: {
* # Removed in 1.0. You can delete this field.
* removed: value
* }
* }
* b: {
* container: {
* # Removed in 1.0. You can delete this field.
* removed: value
* }
* }
* }
* Author's Note:
*
* I am especially fond of this class. If you would like additional transformations
* to be supported by the library, please create an issue on
* GitHub.
* Thanks for your help!
*
*/
@SuppressWarnings("unused")
public class JsonTransformer {
private JsonTransformer() {}
/**
* This resolver is intended for all transformations that occur directly on the object
* being passed into the transformer. Its {@link RootObjectResolver#forEach forEach}
* method is non-recursive and designed for transformers that house additional nested
* object updates.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("{inner:{}}");
* final List resolved = JsonTransformer.root().collect(json);
*
* assert resolved != null;
* assert resolved.size == 1;
* assert resolved.get(0) == json;
* }
*
* @return A new root object resolver to house any global JSON transformations.
*/
public static ObjectResolver root() {
return new RootObjectResolver();
}
/**
* This resolver is intended for transforming all available objects in a given
* JSON object.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("{k1:{},k2:{k3:{}}}");
* final List resolved = JsonTransformer.all().collect(json);
*
* assert resolved != null;
* assert resolved.size == 4;
* }
*
* @return A new object resolver to house transformations on all possible objects.
*/
public static ObjectResolver all() {
return new MatchingObjectResolver(null, (k, o) -> true);
}
/**
* This resolver is intended for any constant object paths nested within the root
* JSON object. It accepts an array of keys which refer objects by name.
* It is capable of resolving all objects that are nested arbitrarily within arrays.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("{inner:[[[{}],{},{}],{},{}]}");
* final List resolved = JsonTransformer.withPath("inner").collect(json);
*
* assert resolved != null;
* assert resolved.size() == 5;
* }
*
* @param path Every key leading up to the objects being transformed.
* @return A new static object resolver for transformations on this path.
*/
public static ObjectResolver withPath(final String... path) {
return new StaticObjectResolver(path);
}
/**
* This resolver is intended for any objects whatsoever that are paired with the given
* key. It can resolve data at any arbitrary depth nested within the root object.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("{a:{x:{x:{}}},b:{x:[{},{}]}}");
* final List resolved = JsonTransformer.scan("x").collect(json);
*
* assert resolved != null;
* assert resolved.size() == 4;
* }
*
* Note: This method will be replaced with global
in CatLib 2.0.
* Expect its behavior to change slightly in the future.
*
* @param key The name of every object being resolved.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver scan(final String key) {
return new MatchingObjectResolver(null, (k, o) -> key.equals(k));
}
/**
* This resolver is intended for any objects containing the given key.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("a:{b:{},c:{}}");
* final List resolved = JsonTransformer.containing("b").collect();
*
* assert resolved != null;
* assert resolved.size() == 1;
* assert resolved.get(0).has("c");
* }
* @param key The key which must be present.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver containing(final String key) {
return new MatchingObjectResolver(null, (k, o) -> o.has(key));
}
/**
* This resolver is intended for any objects containing the given key, provided
* that its value matches the given predicate.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("a:[{b:1},{b:''}]");
* final List resolved = JsonTransformer.containing("b", JsonValue::isNumber).collect();
*
* assert resolved != null;
* assert resolved.size() == 1;
* assert resolved.get(0).get("b").asInt() == 1;
* }
*
* @param key The key which must be present.
* @param predicate The predicate for matching the value of this key.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver containing(final String key, final Predicate predicate) {
return new MatchingObjectResolver(null, (k, o) -> {
final JsonValue value = o.get(key);
return value != null && predicate.test(value);
});
}
/**
* This resolver is intended for matching any condition at all when given a JSON object.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("a:[{b:1,c:2},{d:4,e:5}]");
* final List resolved = JsonTransformer.matching(null, o -> o.has("c") && o.has("d")).collect();
*
* assert resolved != null;
* assert resolved.size() == 1;
* assert resolved.get(0).get("d").asInt() == 4;
* assert resolved.get(0).get("e").asInt() == 5;
* }
*
* @param defaultKey An optional key to be used as the root key.
* @param predicate The condition which any given object must match.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver matching(final @Nullable String defaultKey, final Predicate predicate) {
return new MatchingObjectResolver(defaultKey, (k, o) -> predicate.test(o));
}
/**
* This resolver is intended for matching any condition at all when given a JSON object.
*
* The following code is an assertion of this behavior:
*
* {@code
* final JsonObject json = parse("a:[{b:1,c:2},{d:4}]");
* final List resolved =
* JsonTransformer.matching(null, (k, o) -> "a".equals(k) && o.size() == 2).collect();
*
* assert resolved != null;
* assert resolved.size() == 1;
* assert resolved.get(0).get("b").asInt() == 1;
* assert resolved.get(0).get("c").asInt() == 2;
* }
*
* @param defaultKey An optional key to be used as the root key.
* @param predicate The condition which any given object must match.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver matching(final @Nullable String defaultKey, final ObjectMemberPredicate predicate) {
return new MatchingObjectResolver(defaultKey, predicate);
}
/**
* Variant of {@link JsonTransformer#matching(String, Predicate)} with no explicit
* parameter for defaultKey
.
*
* @param predicate The condition which any given object must match.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver matching(final Predicate predicate) {
return new MatchingObjectResolver(null, (k, o) -> predicate.test(o));
}
/**
* Variant of {@link JsonTransformer#matching(String, ObjectMemberPredicate)} with no
* explicit parameter for defaultKey
.
*
* @param predicate The condition which any given object must match.
* @return A new matching object resolver for all transformations of this kind.
*/
public static ObjectResolver matching(final ObjectMemberPredicate predicate) {
return new MatchingObjectResolver(null, predicate);
}
public static abstract class ObjectResolver {
private final List updates;
private ObjectResolver() {
this.updates = new LinkedList<>();
}
private ObjectResolver(final List updates) {
this.updates = updates;
}
/**
* Bundles an additional transformer into this object. All of its updates will
* be applied in the order in which they are provided.
*
* The following code demonstrates the expected use case for this:
*
* {@code
* static final ObjectResolver OBJECT_A =
* JsonTransformer.withPath("a")
* .history("x", "y", "z")
* .freeze();
*
* static final ObjectResolver OBJECT_B =
* JsonTransformer.withPath("b")
* .history("1", "2", "3")
* .freeze();
*
* static final ObjectResolver TRANSFORMER =
* JsonTransformer.root()
* .include(OBJECT_A)
* .include(OBJECT_B)
* .freeze();
*
* public static void transform(final JsonObject json) {
* TRANSFORMER.updateAll(json);
* }
* }
*
* @param transformer The nested transformer to include
* @return This, for method chaining.
*/
public final ObjectResolver include(final ObjectResolver transformer) {
updates.add(new NestedTransformer(transformer));
return this;
}
/**
* Bundles an additional transformer at the given path. This is intended for nesting
* additional transformations while preserving reuse of the original transformations
* and allowing them to be applied directly to the nested objects.
*
* The following code demonstrates the expected use case for this:
*
* {@code
* static final ObjectResolver OBJECT_A =
* JsonTransformer.root()
* .history("x", "y", "z")
* .freeze();
*
* static final ObjectResolver OBJECT_B =
* JsonTransformer.root()
* .history("1", "2", "3")
* .freeze();
*
* static final ObjectResolver TRANSFORMER =
* JsonTransformer.root()
* .include("a", OBJECT_A)
* .include("b", OBJECT_B)
* .freeze();
*
* public static void transform(final JsonObject json) {
* TRANSFORMER.updateAll(json);
* }
* }
*
* @param path The path to the object being transformed.
* @param transformer The nested transformer to include
* @return This, for method chaining.
*/
public final ObjectResolver include(final String path, final ObjectResolver transformer) {
updates.add(new NestedTransformer(withPath(path).include(transformer)));
return this;
}
/**
* A series of names for any given field over time.
*
* For example, a field with the following history:
*
*
* name1
* name2
* name3
*
*
* Will be transformed to name3
regardless of which name it has in
* the current file.
*
*
* @param names A list of names for the given field over time.
* @return This, for method chaining.
*/
public final ObjectResolver history(final String... names) {
updates.add(new RenameHistory(this, names));
return this;
}
/**
* Collapses the fields from a nested object into its parent.
*
* For example, given the following JSON:
*
* {@code
* outer: {
* inner: {
* a: value
* b: value
* }
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .collapse("outer", "inner")
* .updateAll(json);
* }
*
* The object will be transformed as follows:
*
* {@code
* outer: {
* a: value
* b: value
* }
* }
*
* @param outer The name of the outer object.
* @param inner The name of the inner object.
* @return This, for method chaining.
*/
public final ObjectResolver collapse(final String outer, final String inner) {
updates.add(new PathCollapseHelper(this, outer, inner));
return this;
}
/**
* Converts a JSON value in the following format:
* {@code
* minValue: 0
* maxValue: 1
* }
*
* Into an array as follows:
*
* {@code
* value: [ 0, 1 ]
* }
*
* @param minKey The name of the original key for the minimum value.
* @param minDefault The default value minimum value.
* @param maxKey The name of the original key for the maximum value.
* @param maxDefault The default maximum value.
* @param newKey The new key for the combined value.
* @return This, for method chaining.
*/
public final ObjectResolver toRange(final String minKey, final Number minDefault, final String maxKey,
final Number maxDefault, final String newKey) {
updates.add(new RangeConverter(this, minKey, minDefault, maxKey, maxDefault, newKey));
return this;
}
/**
* Adds a comment indicating that a field has been removed. This is preferable
* to outright removing the field, which the user may not be aware of.
*
* @param key The name of the field being removed.
* @param version The version in which this field was removed.
* @return This, for method chaining.
*/
public final ObjectResolver markRemoved(final String key, final String version) {
updates.add(new RemovedFieldNotifier(this, key, version));
return this;
}
/**
* Renames a value if it matches the given string.
*
* For example, when given the following JSON data:
*
* {@code
* a: [
* {
* b: old1
* }
* {
* b: old2
* }
* ]
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a")
* .renameValue("b", "old1", "new1")
* .renameValue("b", "old2", "new2")
* .updateAll(json);
* }
*
* The object will be transformed as follows:
*
* {@code
* a: [
* {
* b: new1
* }
* {
* b: new2
* }
* ]
* }
*
* @param key The key of the value being renamed
* @param from The original name which has been changed
* @param to The new name for this value.
* @return This, for method chaining.
*/
public final ObjectResolver renameValue(final String key, final String from, final String to) {
updates.add(new FieldRenameHelper(this, key, from, to));
return this;
}
/**
* Applies a generic transformation with the given instructions.
*
* For example, when given the following JSON data:
*
* {@code
* a: {
* old1: old2
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a")
* .transform((k, v) -> Pair.of("new1", JsonValue.valueOf("new2")))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* new1: new2
* }
* }
*
* @param key The name of the field being transformed.
* @param transformation A functional interface for updating the field programmatically.
* @return This, for method chaining.
*/
public final ObjectResolver transform(final String key, final MemberTransformation transformation) {
updates.add(new MemberTransformationHelper(this, key, transformation));
return this;
}
/**
* Exposes the parent object and value in the presence of a given key. This
* allows for manual transformation of various JSON members.
*
* For example, when given the following JSON data:
*
* {@code
* a: {
* k1: v1
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a")
* .ifPresent("k1", (j, v) -> j.add("k2", v))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* k1: v1
* k2: v1
* }
* }
*
* @param key The name of the field being transformed.
* @param f An event to fire for manual transformations in the presence of key
.
* @return This, for method chaining.
*/
public final ObjectResolver ifPresent(final String key, final BiConsumer f) {
updates.add(new MemberPredicateHelper(this, key, f));
return this;
}
/**
* Variant of {@link #ifPresent(String, BiConsumer)} which ignores the present value.
*
* For example, when given the following JSON data:
*
* {@code
* a: {
* k1: v1
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a")
* .ifPresent("k1", j -> j.add("k2", "v2"))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* k1: v1
* k2: v2
* }
* }
*
* @param key The name of the field being transformed.
* @param f An event to fire for manual transformations in the presence of key
.
* @return This, for method chaining.
*/
public final ObjectResolver ifPresent(final String key, final Consumer f) {
updates.add(new MemberPredicateHelper(this, key, (j, v) -> f.accept(j)));
return this;
}
/**
* Transfers the contents of an expected array, if present, into a different array,
* regardless of whether it is present.
*
* For example, when given the following JSON data:
*
* {@code
* a: {
* a1: [ 1, 2, 3 ]
* o1: [ 4, 5, 6 ]
* o2: [ 7, 8, 9 ]
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.withPath("a")
* .moveArray("o1", "a1")
* .moveArray("o2", "a2")
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* a: {
* a1: [ 1, 2, 3, 4, 5, 6 ]
* a2: [ 7, 8, 9 ]
* }
* }
*
* @param from The name of the source array.
* @param to The name of the destination array.
* @return This, for method chaining.
*/
public final ObjectResolver moveArray(final String from, final String to) {
updates.add(new ArrayCopyHelper(this, from, to));
return this;
}
/**
* Relocates fields to entirely different object paths. This transformer is only valid for
* regular object paths. If any arrays are present in the given path, the first object in
* the flattened array path will be used and any other objects will be silently ignored.
*
* For example, when given the following JSON data:
*
* {@code
* {
* path: {
* to: {
* value: 24
* }
* }
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .relocate("path.to.value", "whole.new.path")
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* whole: {
* new: {
* path: 24
* }
* }
* }
* }
*
* Note that when any object arrays are present on the to
path, only the
* first object in the array will copied into. The remaining data will be ignored and
* the following transformation will occur:
*
* {@code
* {
* path: {
* to: {
* value: 24
* }
* }
* whole: {
* new: [
* {
* other: 48
* }
* {
* other: 49
* }
* ]
* }
* }
* }
*
* Will be transformed into:
*
* {@code
* {
* whole: {
* new: [
* {
* other: 48
* path: 24
* }
* {
* other: 49
* }
* ]
* }
* }
* }
*
* Note that when any object arrays are present on the from
path, only the
* first object in the array will be copied out of. This is considered an error condition
* and the remaining data will be left behind.
*
*
* @param from The fully-qualified, dotted path to the original value.
* @param to The fully-qualified, dotted path to the new value.
* @return This, for method, chaining.
*/
public final ObjectResolver relocate(final String from, final String to) {
return relocate(from, to, true);
}
/**
* Variant of {@link #relocate(String, String)} which is designed to cover the condition where
* an object or array is being moved and another container already exists at the destination.
* Setting the third parameter (merge
) to true
will merge containers
* instead of overwriting them.
*
* For example, when given the following JSON data:
*
* {@code
* {
* simple: {
* path: {
* a: 1
* b: 2
* }
* }
* other: {
* path: {
* c: 3
* }
* }
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .relocate("simple.path", "other.path", true)
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* other: {
* path: {
* a: 1
* b: 2
* c: 3
* }
* }
* }
* }
*
* @param from The fully-qualified, dotted path to the original value.
* @param to The fully-qualified, dotted path to the new value.
* @param merge Whether to write into any existing containers at the end of the path.
* @return This, for method, chaining.
*/
public final ObjectResolver relocate(final String from, final String to, final boolean merge) {
updates.add(new FieldRelocator(this, from, to, merge));
return this;
}
/**
* Places the fields in every matching object into a specific order. Any fields
* not matched by the set will simply be rendered at the end.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* first: 9
* b: 2
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .reorder(singleton("first")
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* first: 9
* a: 1
* b: 2
* }
* }
*
* @param keys The exact order in which to place the fields.
* @return This, for method chaining.
*/
public final ObjectResolver reorder(final Collection keys) {
return this.reorder(keys, Collections.emptyList());
}
/**
* Places the fields in a strict, specific order. This method accepts two sets of
* keys, where the first places any matching fields at the top and the second
* places any matching fields at the bottom.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* first: 8
* b: 2
* last: 9
* c: 3
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .reorder(singleton("first"), singleton("last"))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* first: 8
* a: 1
* b: 2
* c: 3
* last: 9
* }
* }
*
* @param first The keys to display at the beginning of the object.
* @param last The keys to display at the end of the object.
* @return This, for method chaining.
*/
public final ObjectResolver reorder(final Collection first, final Collection last) {
updates.add(new StrictFieldOrganizer(this, first, last));
return this;
}
/**
* Sorts every field in the matching objects in alphabetical order.
*
* For example, when given the following JSON data:
*
* {@code
* {
* b: 2
* c: 3
* a: 1
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .sort()
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* a: 1
* b: 2
* c: 3
* }
* }
*
* @return This, for method chaining.
*/
public final ObjectResolver sort() {
return this.sort(Comparator.comparing(JsonObject.Member::getKey));
}
/**
* Sorts every field in the matching objects when given a specific comparator
* to determine their order.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* b: 2
* c: 3
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .sort((m1, m2) -> m2.getName().compareTo(m1.getName()))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* c: 3
* b: 2
* a: 1
* }
* }
*
* @param comparator A comparator to determine field order.
* @return This, for method chaining.
*/
public final ObjectResolver sort(final Comparator comparator) {
updates.add(new FieldSorter(this, comparator));
return this;
}
/**
* Recursively provides default values in the matching objects. Existing values will
* not be replaced by this transformer.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 0
* c: 4
* n: {}
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .setDefaults(parse("a:1,b:2,c:3,n:{k:9}"))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* a: 0
* b: 2
* c: 4
* n: {
* k: 9
* }
* }
* }
*
* @param defaults An object containing the default values.
* @return This, for method chaining.
*/
public final ObjectResolver setDefaults(final JsonObject defaults) {
updates.add(new DefaultFieldProvider(this, defaults));
return this;
}
/**
* Removes any single key from the matching objects.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* b: 2
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .remove("a")
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* b: 2
* }
* }
*
* @param key The name of the field being removed.
* @return This, for method chaining.
*/
public final ObjectResolver remove(final String key) {
return this.remove(key, null);
}
/**
* Removes a single key-value pair from the matching objects. For this transformer
* to remove a field, the value of the field must match value
.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* b: 2
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .remove("a", 1)
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* b: 2
* }
* }
*
* However, the field will not be removed if the value does not match.
*
*
* @param key The name of the field being removed.
* @param value The expected value being removed.
* @return This, for method chaining.
*/
public final ObjectResolver remove(final String key, final Object value) {
return this.remove(key, Json.any(value));
}
/**
* Removes a set of key-value pairs from the matching object. An ideal use case for this
* transformer would be whenever a default JSON object is available. The default object
* could be used as a mask to remove unnecessary fields.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* b: 2
* c: 3
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .remove(parse("a:1,b:1,c:1"))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* b: 2
* c: 3
* }
* }
*
* @param json A set of key-value pairs being removed.
* @return This, for method chaining.
*/
public final ObjectResolver remove(final JsonObject json) {
for (final JsonObject.Member m : json) {
this.remove(m.getKey(), m.getValue());
}
return this;
}
/**
* Removes a single key-value pair from the matching objects. This method is a variant
* of {@link #remove(String, Object)} accepting a {@link JsonValue} directly.
*
* For example, when given the following JSON data:
*
* {@code
* {
* a: 1
* b: 2
* }
* }
*
* And the following history:
*
* {@code
* JsonTransformer.root()
* .remove("a", JsonValue.valueOf(1))
* .updateAll(json)
* }
*
* The object will be transformed as follows:
*
* {@code
* {
* b: 2
* }
* }
*
* However, the field will not be removed if the value does not match.
*
*
* @param key The name of the field being removed.
* @param value The expected value being removed.
* @return This, for method chaining.
*/
public final ObjectResolver remove(final String key, @Nullable final JsonValue value) {
updates.add(new FieldRemover(this, key, value));
return this;
}
/**
* Runs the given consumer for each matching object, when available.
*
* @param fn The function to execute for each matching object.
* @return This, for method chaining.
*/
public final ObjectResolver run(final Consumer fn) {
updates.add(new SimpleFunctionRunner(this, fn));
return this;
}
/**
* Prevents any further changes to this resolver. Use this to clearly indicate the
* purpose of any static json transformer and guarantee thread safety.
*
* @return A new object resolver which cannot be mutated.
*/
public final ObjectResolver freeze() {
return new FrozenObjectResolver(this);
}
/**
* Runs every transformation defined on the given JSON object.
*
* @param json The JSON object target for these transformations.
*/
public final void updateAll(final JsonObject json) {
for (final Updater update : updates) {
update.update(json);
}
}
/**
* Runs every transformation on multiple JSON objects in order.
*
* @param objects The JSON object targets for these transformations.
*/
public final void updateAll(final JsonObject... objects) {
for (final JsonObject json : objects) {
this.updateAll(json);
}
}
/**
* Runs every transformation without modifying the given object. Instead, a new value
* will be returned.
*
* @param json The JSON object source for this operation.
* @return A new JSON object with the provided transforms applied.
*/
public final JsonObject getUpdated(final JsonObject json) {
final JsonObject clone = json.deepCopy().asObject();
this.updateAll(clone);
return clone;
}
/**
* Runs every transformation on multiple JSON objects without modifying any
* source object.
*
* @param objects The JSON object sources for this operation.
* @return An array of new JSON objects with the provided transforms applied.
*/
public final JsonObject[] getUpdated(final JsonObject... objects) {
final JsonObject[] clones = new JsonObject[objects.length];
for (int i = 0; i < objects.length; i++) {
clones[i] = this.getUpdated(objects[i]);
}
return clones;
}
/**
* Executes an operation for each last container when given a path. Each path element
* is treated as either an object or an array.
*
* @param json The parent JSON file being operated on.
* @param fn What to do for each element at path[path.length - 1]
*/
public abstract void forEach(JsonObject json, Consumer fn);
/**
* Collects every matching object within the given root into an array.
*
* @param json The root JSON object containing the expected data.
* @return A list of every matching object.
*/
public final List collect(final JsonObject json) {
return this.collect(json, new ArrayList<>());
}
/**
* Collects every matching object within the given root into a collection of any type.
*
* @param json The root JSON object containing the expected data.
* @param collection The collection being written into.
* @param The type of collection being written into.
* @return A collection of every matching object.
*/
public final > T collect(final JsonObject json, final T collection) {
this.forEach(json, collection::add);
return collection;
}
}
public static class RootObjectResolver extends ObjectResolver {
private RootObjectResolver() {}
@Override
public void forEach(final JsonObject json, final Consumer fn) {
fn.accept(json);
}
}
public static class StaticObjectResolver extends ObjectResolver {
private final String[] path;
private StaticObjectResolver(final String[] path) {
this.path = path;
}
@Override
public void forEach(final JsonObject json, final Consumer fn) {
forEachContainer(json, 0, fn);
}
private void forEachContainer(final JsonObject container, final int index, final Consumer fn) {
if (index < path.length) {
for (JsonObject o : XjsUtils.getRegularObjects(container, path[index])) {
forEachContainer(o, index + 1, fn);
}
} else if (index == path.length) {
fn.accept(container);
}
}
}
public interface ObjectMemberPredicate {
boolean test(final @Nullable String key, final JsonObject json);
}
public static class MatchingObjectResolver extends ObjectResolver {
private final @Nullable String defaultKey;
private final ObjectMemberPredicate predicate;
private MatchingObjectResolver(final @Nullable String defaultKey, final ObjectMemberPredicate predicate) {
this.defaultKey = defaultKey;
this.predicate = predicate;
}
@Override
public void forEach(final JsonObject json, final Consumer fn) {
this.forEachInObject(this.defaultKey, json, fn);
}
private void forEachInObject(final String key, final JsonObject json, final Consumer fn) {
if (this.predicate.test(key, json)) {
fn.accept(json);
}
for (final JsonObject.Member member : json) {
final String name = member.getKey();
final JsonValue value = member.getValue();
if (value.isObject()) {
forEachInObject(name, value.asObject(), fn);
} else if (value.isArray()) {
forEachInArray(name, value.asArray(), fn);
}
}
}
private void forEachInArray(final String key, final JsonArray array, final Consumer fn) {
for (final JsonValue value : array) {
if (value.isObject()) {
forEachInObject(key, value.asObject(), fn);
} else if (value.isArray()) {
forEachInArray(key, value.asArray(), fn);
}
}
}
}
public static class FrozenObjectResolver extends ObjectResolver {
private final ObjectResolver wrapped;
private FrozenObjectResolver(final ObjectResolver wrapped) {
super(Collections.unmodifiableList(wrapped.updates));
this.wrapped = wrapped;
}
@Override
public void forEach(final JsonObject json, final Consumer fn) {
wrapped.forEach(json, fn);
}
}
public interface Updater {
void update(final JsonObject json);
}
public static class NestedTransformer implements Updater {
private final ObjectResolver resolver;
private NestedTransformer(final ObjectResolver resolver) {
this.resolver = resolver;
}
@Override
public void update(final JsonObject json) {
resolver.updateAll(json);
}
}
public static class RenameHistory implements Updater {
private final ObjectResolver resolver;
private final String[] history;
private RenameHistory(final ObjectResolver resolver, final String[] history) {
this.resolver = resolver;
this.history = history;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::renameFields);
}
private void renameFields(final JsonObject json) {
final String current = history[history.length - 1];
for (int i = 0; i < history.length - 1; i++) {
final String key = history[i];
final JsonValue original = json.get(key);
if (original != null) {
json.set(current, original);
json.remove(key);
}
}
}
}
public static class PathCollapseHelper implements Updater {
private final ObjectResolver resolver;
private final String outer;
private final String inner;
private PathCollapseHelper(final ObjectResolver resolver, final String outer, final String inner) {
this.resolver = resolver;
this.outer = outer;
this.inner = inner;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::collapse);
}
private void collapse(final JsonObject json) {
final JsonValue outerValue = json.get(outer);
if (outerValue != null && outerValue.isObject()) {
final JsonValue innerValue = outerValue.asObject().get(inner);
if (innerValue != null) {
json.set(outer, innerValue);
}
}
}
}
public static class RangeConverter implements Updater {
private final ObjectResolver resolver;
private final String minKey;
private final Number minDefault;
private final String maxKey;
private final Number maxDefault;
private final String newKey;
private RangeConverter(final ObjectResolver resolver, final String minKey, final Number minDefault,
final String maxKey, final Number maxDefault, final String newKey) {
this.resolver = resolver;
this.minKey = minKey;
this.minDefault = minDefault;
this.maxKey = maxKey;
this.maxDefault = maxDefault;
this.newKey = newKey;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::convert);
}
private void convert(final JsonObject json) {
if (json.has(minKey) || json.has(maxKey)) {
if (minDefault instanceof Double || minDefault instanceof Float) {
final float min = json.getOptional(minKey, JsonValue::asFloat).orElse(minDefault.floatValue());
final float max = json.getOptional(maxKey, JsonValue::asFloat).orElse(maxDefault.floatValue());
json.set(newKey, getRange(min, max));
} else {
final int min = json.getOptional(minKey, JsonValue::asInt).orElse(minDefault.intValue());
final int max = json.getOptional(maxKey, JsonValue::asInt).orElse(maxDefault.intValue());
json.set(newKey, getRange(min, max));
}
json.remove(minKey);
json.remove(maxKey);
}
}
private JsonValue getRange(final float min, final float max) {
if (min == max) {
return Json.value(min);
}
return new JsonArray().add(min).add(max).condense();
}
private JsonValue getRange(final int min, final int max) {
if (min == max) {
return Json.value(min);
}
return new JsonArray().add(min).add(max).condense();
}
}
public static class RemovedFieldNotifier implements Updater {
private final ObjectResolver resolver;
private final String key;
private final String version;
private RemovedFieldNotifier(final ObjectResolver resolver, final String key, final String version) {
this.resolver = resolver;
this.key = key;
this.version = version;
}
@Override
public void update(JsonObject json) {
resolver.forEach(json, this::markRemoved);
}
private void markRemoved(JsonObject json) {
final JsonValue value = json.get(key);
if (value != null) {
json.setLineLength(1);
value.setComment(CommentType.EOL, f("Removed in {}. You can delete this field.", version));
}
}
}
public static class FieldRenameHelper implements Updater {
private final ObjectResolver resolver;
private final String key;
private final String from;
private final String to;
private FieldRenameHelper(final ObjectResolver resolver, final String key, final String from, final String to) {
this.resolver = resolver;
this.key = key;
this.from = from;
this.to = to;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::renameValue);
}
private void renameValue(final JsonObject json) {
final JsonValue value = json.get(key);
if (value != null && value.isString() && from.equalsIgnoreCase(value.asString())) {
json.set(key, to);
}
}
}
@FunctionalInterface
public interface MemberTransformation {
Pair transform(final String name, final JsonValue value);
}
public static class MemberTransformationHelper implements Updater {
private final ObjectResolver resolver;
private final String key;
private final MemberTransformation transformation;
private MemberTransformationHelper(final ObjectResolver resolver, final String key,
final MemberTransformation transformation) {
this.resolver = resolver;
this.key = key;
this.transformation = transformation;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::transform);
}
private void transform(final JsonObject json) {
final JsonValue value = json.get(key);
if (value != null) {
final Pair updated = transformation.transform(key, value);
json.remove(key);
json.set(updated.getKey(), updated.getValue());
}
}
}
public static class MemberPredicateHelper implements Updater {
private final ObjectResolver resolver;
private final String key;
private final BiConsumer ifPresent;
private MemberPredicateHelper(final ObjectResolver resolver, final String key,
final BiConsumer ifPresent) {
this.resolver = resolver;
this.key = key;
this.ifPresent = ifPresent;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::test);
}
private void test(final JsonObject json) {
final JsonValue value = json.get(key);
if (value != null) {
ifPresent.accept(json, value);
}
}
}
public static class ArrayCopyHelper implements Updater {
private final ObjectResolver resolver;
private final String from;
private final String to;
private ArrayCopyHelper(final ObjectResolver resolver, final String from, final String to) {
this.resolver = resolver;
this.from = from;
this.to = to;
}
@Override
public void update(JsonObject json) {
resolver.forEach(json, this::copy);
}
private void copy(final JsonObject json) {
final JsonValue source = json.get(from);
if (source != null) {
final JsonArray array = XjsUtils.getOrCreateArray(json, to);
for (final JsonValue value : source.intoArray()) {
array.add(value);
}
json.remove(from);
}
}
}
public static class FieldRelocator implements Updater {
private final ObjectResolver resolver;
private final String[] from;
private final String[] to;
private final boolean merge;
private FieldRelocator(final ObjectResolver resolver, final String from, final String to, final boolean merge) {
this.resolver = resolver;
this.from = from.split("\\.");
this.to = to.split("\\.");
this.merge = merge;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::relocate);
}
private void relocate(final JsonObject json) {
final JsonValue toMove = this.resolve(json);
if (toMove == null) return;
for (final JsonObject container : this.getContainers(json)) {
final String key = to[to.length - 1];
final JsonValue get = container.get(key);
if (get == null) {
container.add(key, toMove);
} else if (merge) {
if (get.isObject() && toMove.isObject()) {
toMove.asObject().forEach(m -> get.asObject().set(m.getKey(), m.getValue()));
} else if (get.isArray() && toMove.isArray()) {
toMove.asArray().forEach(v -> get.asArray().add(v));
} else {
container.set(key, toMove);
}
} else {
container.set(key, toMove);
}
}
}
@Nullable
private JsonValue resolve(final JsonObject json) {
JsonObject parent = json;
for (int i = 0; i < from.length - 1; i++) {
// Only use the first object. All others are ambiguous.
final JsonValue next = parent.get(from[i]);
if (next == null) return null;
final JsonObject object = this.getFirstObject(next);
if (object == null) return null;
parent = object;
}
final String key = from[from.length - 1];
final JsonValue get = parent.get(key);
if (get != null) parent.remove(key);
return get;
}
@Nullable
private JsonObject getFirstObject(final JsonValue value) {
if (value.isObject()) {
return value.asObject();
} else if (value.isArray()) {
final JsonArray array = value.asArray();
if (!array.isEmpty()) {
return getFirstObject(array.get(0));
}
}
return null;
}
private List getContainers(final JsonObject json) {
final List containers = new ArrayList<>();
this.addContainers(containers, json, 0);
return containers;
}
private void addContainers(final List containers, final JsonObject root, final int index) {
if (index < to.length - 1) {
final String key = to[index];
final JsonValue value = root.get(key);
if (value != null && value.isObject()) {
this.addContainers(containers, value.asObject(), index + 1);
} else if (value != null && value.isArray()) {
this.addToArray(containers, value.asArray(), index);
} else {
final JsonObject object = new JsonObject();
root.add(key, object);
this.addContainers(containers, object, index + 1);
}
} else if (index == to.length - 1) {
containers.add(root);
}
}
private void addToArray(final List containers, final JsonArray array, final int index) {
if (array.isEmpty()) {
final JsonObject object = new JsonObject();
array.add(object);
this.addContainers(containers, object, index + 1);
} else {
for (final JsonValue value : array) {
if (value != null && value.isObject()) {
this.addContainers(containers, value.asObject(), index + 1);
} else if (value != null && value.isArray()) {
this.addToArray(containers, value.asArray(), index);
}
}
}
}
}
public static class StrictFieldOrganizer implements Updater {
final ObjectResolver resolver;
final Collection first;
final Collection last;
private StrictFieldOrganizer(final ObjectResolver resolver, final Collection first, final Collection last) {
this.resolver = resolver;
this.first = first;
this.last = last;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::reorder);
}
private void reorder(final JsonObject json) {
final JsonObject clone = new JsonObject().addAll(json);
final JsonObject firstValues = drain(clone, first);
final JsonObject lastValues = drain(clone, last);
json.clear();
json.addAll(firstValues);
json.addAll(clone);
json.addAll(lastValues);
}
private static JsonObject drain(final JsonObject source, final Collection keys) {
final JsonObject drain = new JsonObject();
for (final String key : keys) {
final JsonValue value = source.get(key);
if (value != null) {
drain.add(key, value);
source.remove(key);
}
}
return drain;
}
}
public static class FieldSorter implements Updater {
final ObjectResolver resolver;
final Comparator comparator;
private FieldSorter(final ObjectResolver resolver, final Comparator comparator) {
this.resolver = resolver;
this.comparator = comparator;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::sort);
}
private void sort(final JsonObject json) {
final JsonObject sorted = json.stream().sorted(comparator).collect(JsonCollectors.member());
json.clear().addAll(sorted);
}
}
public static class DefaultFieldProvider implements Updater {
final ObjectResolver resolver;
final JsonObject defaults;
private DefaultFieldProvider(final ObjectResolver resolver, final JsonObject defaults) {
this.resolver = resolver;
this.defaults = defaults;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::setDefaults);
}
private void setDefaults(final JsonObject json) {
json.setDefaults(this.defaults);
}
}
public static class FieldRemover implements Updater {
final ObjectResolver resolver;
final String key;
@Nullable final JsonValue value;
private FieldRemover(final ObjectResolver resolver, final String key, @Nullable final JsonValue value) {
this.resolver = resolver;
this.key = key;
this.value = value;
}
@Override
public void update(final JsonObject json) {
resolver.forEach(json, this::remove);
}
private void remove(final JsonObject json) {
if (this.value == null || this.value.equals(json.get(this.key))) {
json.remove(this.key);
}
}
}
public static class SimpleFunctionRunner implements Updater {
final ObjectResolver resolver;
final Consumer fn;
private SimpleFunctionRunner(final ObjectResolver resolver, final Consumer fn) {
this.resolver = resolver;
this.fn = fn;
}
@Override
public void update (final JsonObject json) {
resolver.forEach(json, fn);
}
}
}