Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/main/java/org/hisp/dhis/jsontree/Collapsed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.hisp.dhis.jsontree;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marker annotation for {@link java.lang.reflect.RecordComponent}s that are themselves {@link
* Record}s whose components should be treated as if they were directly defined in the parent record
* type when mapping JSON to Java using {@link JsonValue#to(Class)}.
*
* <p>Collapsing can e.g. be used to access a "flat" JSON object with many properties as a Java
* record which uses inner records to group those properties without the need to mirror that
* grouping in JSON.
*
* <p>Since records do not allow for inheritance collapsing inner records can be used to compose
* structures that certain groups of properties without the need to duplicate the components. This
* is important as it still allows to write code that operates on one of such groups of properties
* in a way that inheritance would without the need to duplicate the code that can handle data
* having those properties. In contrast to inheritance collapsed composition supports the equivalent
* of multiple-inheritance simply by having multiple inner collapsed record properties.
*
* <p>Please note that ATM records with generics are not supported simply to keep the implementation
* simple.
*
* @author Jan Bernitt
* @since 1.9
*/
@Target(ElementType.RECORD_COMPONENT)
@Retention(RetentionPolicy.RUNTIME)
public @interface Collapsed {}
22 changes: 6 additions & 16 deletions src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,9 @@ default boolean exists(CharSequence name) {
* @since 0.11 (as Stream)
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
default Stream<Text> keys() {
if (isUndefined() || isEmpty()) return Stream.empty();
return node().keys().stream();
}

/**
* @return a stream of the map/object values in order of their declaration
* @throws JsonTreeException in case this node does exist but is not an object node
* @since 0.11
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
default Stream<E> values() {
return entries();
default Streamable.Sized<Text> keys() {
if (isUndefined() || isEmpty()) return Streamable.empty();
return node().keys();
}

/**
Expand All @@ -146,9 +136,9 @@ default Stream<E> values() {
* @since 0.11
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
default Stream<E> entries() {
if (isUndefined() || isEmpty()) return Stream.empty();
return node().keys().stream().map(this::get);
default Streamable.Sized<E> entries() {
if (isUndefined() || isEmpty()) return Streamable.empty();
return node().keys().map(this::get);
}

/**
Expand Down
16 changes: 11 additions & 5 deletions src/main/java/org/hisp/dhis/jsontree/JsonAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ public static Stream<?> accessAsStream(JsonMixed stream, Type as, JsonAccessors
JsonAccessor<?> elements = accessors.accessor(getRawType(elementType));
// auto-box simple values in a 1 element sequence
if (!stream.isArray()) return Stream.of(elements.access(stream, as, accessors));
return stream.stream(Index.AUTO_SKIP)
.map(e -> elements.access(e.as(JsonMixed.class), elementType, accessors));
return stream.values(Index.AUTO_SKIP)
.map(e -> elements.access(e.as(JsonMixed.class), elementType, accessors)).stream();
}

@SuppressWarnings({"java:S1168", "java:S1452"})
Expand Down Expand Up @@ -299,6 +299,7 @@ public static Stream<?> accessAsStream(JsonMixed stream, Type as, JsonAccessors
* @param components the components in that constructor to pass as args
* @param defaults default arguments if the component is undefined in JSON, null of no defaults
* are available (taken from a constant with name {@code DEFAULT}).
* @param collapsed true, if the component was annotated with {@link Collapsed}
* @param of a static method to construct a record from a single argument (typical {@code
* of}-method)
* @param ofArgType the generic argument type of the {@link #of()}-method
Expand All @@ -307,6 +308,7 @@ private record NewRecord(
MethodHandle constructor,
RecordComponent[] components,
Object[] defaults,
boolean[] collapsed,
MethodHandle of,
Type ofArgType) {}

Expand Down Expand Up @@ -340,11 +342,12 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access

Object[] args = new Object[components.length];
Object[] defaults = newRecord.defaults;
boolean[] collapsed = newRecord.collapsed;
boolean hasDefaults = defaults != null;
if (obj.isObject()) {
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
JsonMixed cValue = obj.get(c.getName());
JsonMixed cValue = collapsed[i] ? obj : obj.get(c.getName());
args[i] =
hasDefaults && cValue.isUndefined()
? defaults[i]
Expand All @@ -353,7 +356,7 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access
} else if (obj.isArray()) {
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
JsonMixed cValue = obj.get(i);
JsonMixed cValue = collapsed[i] ? obj : obj.get(i);
args[i] =
hasDefaults && cValue.isUndefined()
? defaults[i]
Expand Down Expand Up @@ -399,6 +402,9 @@ private static NewRecord createRecordFactory(
throw new JsonAccessException(
"JSON cannot be mapped to Java record %s, canonical constructor is not accessible"
.formatted(type.getSimpleName()));
boolean[] collapsed = new boolean[components.length];
for (int i = 0; i < components.length; i++)
collapsed[i] = components[i].isAnnotationPresent(Collapsed.class);
MethodHandle ofMethod = null;
Type ofMethodArgType = null;
if (components.length > 1) {
Expand All @@ -411,7 +417,7 @@ private static NewRecord createRecordFactory(
}
}
}
return new NewRecord(canonical, components, defaults(type), ofMethod, ofMethodArgType);
return new NewRecord(canonical, components, defaults(type), collapsed, ofMethod, ofMethodArgType);
}

private static MethodHandle constructor(
Expand Down
100 changes: 44 additions & 56 deletions src/main/java/org/hisp/dhis/jsontree/JsonArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
import static org.hisp.dhis.jsontree.JsonNode.Index.AUTO;
import static org.hisp.dhis.jsontree.JsonNode.Index.SKIP;

import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import org.hisp.dhis.jsontree.JsonNode.Index;
import org.hisp.dhis.jsontree.internal.NotNull;
import org.hisp.dhis.jsontree.internal.TerminalOp;

/**
Expand All @@ -56,38 +58,59 @@ default JsonArray getValue() {
return this; // return type override
}

/**
* Index access to the array.
*
* <p>Note that this will neither check index nor element type.
*
* @param index index to access (0 and above)
* @param as assumed type of the element
* @param <E> type of the returned element
* @return element at the given index
*/
<E extends JsonValue> E get(int index, Class<E> as);

@Override
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default @NotNull Iterator<JsonMixed> iterator() {
return values(AUTO).iterator();
}

@Override
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default Stream<JsonMixed> stream() {
return stream(AUTO);
return values(AUTO).stream();
}

/**
* @implNote This utilizes {@link JsonNode#elements(Index)} avoiding map lookups for each element.
* On {@link JsonAbstractArray} level this cannot be done as the node cannot be {@link
* JsonNode#lift(JsonAccessors)} ed to the unknown generic target type.
* @param index the strategy to apply when it comes to node lookup and indexing
* @see #values(Index)
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default Stream<JsonMixed> stream(JsonNode.Index index) {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return Stream.empty();
JsonAccessors accessors = getAccessors();
return node.elements(index).stream().map(n -> n.lift(accessors));
default Streamable.Sized<JsonMixed> values() {
return values(SKIP);
}

/**
* Index access to the array.
*
* <p>Note that this will neither check index nor element type.
*
* @param index index to access (0 and above)
* @param as assumed type of the element
* @param <E> type of the returned element
* @return element at the given index
* @see #values()
* @since 1.9
*/
<E extends JsonValue> E get(int index, Class<E> as);
default <T> Streamable.Sized<T> values(Class<T> to) {
return values().map(e -> e.to(to));
}

/**
* @return this arrays values as {@link org.hisp.dhis.jsontree.Streamable.Sized}
* @throws JsonTreeException if this node exist but is not an array or null node
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default Streamable.Sized<JsonMixed> values(JsonNode.Index index) {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return Streamable.empty();
JsonAccessors accessors = getAccessors();
return node.elements(index).map(n -> n.lift(accessors));
}

/**
* @return the array elements as a uniform list of {@link String}
Expand All @@ -97,7 +120,7 @@ default Stream<JsonMixed> stream(JsonNode.Index index) {
default List<String> stringValues() {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return List.of();
return node.elements(JsonNode.Index.SKIP).stream()
return node.elements(SKIP)
.map(JsonNode::textValue)
.map(Text::toString)
.toList();
Expand All @@ -111,18 +134,7 @@ default List<String> stringValues() {
default List<Boolean> booleanValues() {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return List.of();
return node.elements(JsonNode.Index.SKIP).stream().map(JsonNode::booleanValue).toList();
}

@TerminalOp(canBeUndefined = true, mustBeArray = true)
default <E> List<E> values(Function<String, E> f) {
JsonNode node = nodeIfExists();
if (node == null || node.isNull()) return List.of();
return node.elements(JsonNode.Index.SKIP).stream()
.map(JsonNode::textValue)
.map(Text::toString)
.map(f)
.toList();
return node.elements(SKIP).map(JsonNode::booleanValue).toList();
}

/**
Expand Down Expand Up @@ -158,30 +170,6 @@ default DoubleStream doubleValues() {
return node.elements(SKIP).stream().mapToDouble(JsonNode::doubleValue);
}

/**
* Uses {@link #getAccessors()} for type conversion.
*
* @param to element target type
* @return a stream of all array values accessed as the given type in the order defined in JSON
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default <E> Stream<E> streamValues(Class<E> to) {
return stream(SKIP).map(e -> e.to(to));
}

/**
* Uses {@link #getAccessors()} for type conversion.
*
* @param to element target type
* @return a list of all array values accessed as the given type in the order defined in JSON
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeArray = true)
default <E> List<E> listValues(Class<E> to) {
return streamValues(to).toList();
}

default JsonMixed get(int index) {
return get(index, JsonMixed.class);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/hisp/dhis/jsontree/JsonDiff.java
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private static void diffObject(
JsonObject e, JsonObject a, Mode mode, Consumer<Difference> add, PropertyInfo p) {
if (!p.anyAdditional(mode.objects.anyAdditional)) {
// list all extra members
a.keys()
a.keys().stream()
.filter(not(e::has))
.forEach(key -> add.accept(new Difference(Type.MORE, e.get(key), a.get(key))));
}
Expand All @@ -211,7 +211,7 @@ private static void diffObject(
} else {
// exact order
Iterator<Text> eKeys = e.keys().iterator();
Iterator<Text> aKeys = a.keys().filter(e::has).iterator();
Iterator<Text> aKeys = a.keys().stream().filter(e::has).iterator();
while (eKeys.hasNext() && aKeys.hasNext()) {
Text eKey = eKeys.next();
Text aKey = aKeys.next();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/hisp/dhis/jsontree/JsonMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public interface JsonMap<E extends JsonValue> extends JsonAbstractObject<E> {
* @since 0.11
*/
default <T> Map<String, T> toMap(Function<E, T> toValue) {
return entries().collect(Collectors.toMap(e -> e.getKey().toString(), toValue));
return entries().stream().collect(Collectors.toMap(e -> e.getKey().toString(), toValue));
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/org/hisp/dhis/jsontree/JsonMixed.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
*/
@Validation(type = {OBJECT, ARRAY, STRING, NUMBER, INTEGER, BOOLEAN})
@Validation.Ignore
public interface JsonMixed
extends JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonInteger {
public interface JsonMixed extends JsonObject, JsonArray, JsonPrimitive {

/**
* Lift an actual {@link JsonNode} tree to a virtual {@link JsonValue}.
Expand Down
14 changes: 7 additions & 7 deletions src/main/java/org/hisp/dhis/jsontree/JsonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ default JsonNode memberIfExists(Text name) throws JsonPathException {
* @return this {@link #value()} as a sequence of {@link Entry}s
* @throws JsonTreeException if this node is not an object node that could have members
*/
default Streamable<JsonNode> members() {
default Streamable.Sized<JsonNode> members() {
throw notA(OBJECT, this, "members()");
}

Expand All @@ -593,7 +593,7 @@ default Streamable<JsonNode> members() {
* @throws JsonTreeException if this node is not an object node that could have members
* @since 0.11
*/
default Streamable<Text> keys() {
default Streamable.Sized<Text> keys() {
throw notA(OBJECT, this, "keys()");
}

Expand All @@ -604,7 +604,7 @@ default Streamable<Text> keys() {
* @throws JsonTreeException if this node is not an object node that could have members
* @since 1.2
*/
default Streamable<JsonPath> paths() {
default Streamable.Sized<JsonPath> paths() {
throw notA(OBJECT, this, "paths()");
}

Expand All @@ -619,7 +619,7 @@ default Streamable<JsonPath> paths() {
* internally will be reused and returned by the iterator.
* @throws JsonTreeException if this node is not an object node that could have members
*/
default Streamable<JsonNode> members(Index index) {
default Streamable.Sized<JsonNode> members(Index index) {
throw notA(OBJECT, this, "members(Index)");
}

Expand Down Expand Up @@ -674,7 +674,7 @@ default JsonNode elementIfExists(int index) throws JsonPathException, JsonTreeEx
* @return this {@link #value()} as as {@link Stream}
* @throws JsonTreeException if this node is not an array node that could have elements
*/
default Streamable<JsonNode> elements() {
default Streamable.Sized<JsonNode> elements() {
throw notA(ARRAY, this, "elements()");
}

Expand All @@ -689,7 +689,7 @@ default Streamable<JsonNode> elements() {
* already exist internally will be reused and returned by the iterator.
* @throws JsonTreeException if this node is not an array node that could have elements
*/
default Streamable<JsonNode> elements(Index index) {
default Streamable.Sized<JsonNode> elements(Index index) {
throw notA(ARRAY, this, "elements(Index)");
}

Expand All @@ -702,7 +702,7 @@ default Streamable<JsonNode> elements(Index index) {
* @throws JsonTreeException if this node is not an array node that could have elements
* @since 1.9
*/
default Streamable<Text> values() {
default Streamable.Sized<Text> values() {
throw notA(ARRAY, this, "values()");
}

Expand Down
Loading
Loading