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
240 changes: 240 additions & 0 deletions INPUT_EXPRESSIONS.md

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions src/main/java/org/hisp/dhis/jsontree/InputExpression.java

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions src/main/java/org/hisp/dhis/jsontree/JsonAbstractArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,22 @@ default E first(Predicate<E> test) {

/**
* @param schema the schema to validate all elements of this array against
* @param mode check, fail fast or give a breakdown of all issues?
* @param rules optional set of {@link Rule}s to check, empty includes all
* @return A stream with errors for each element of this array if {@link Validation.Mode} is
* probing
* @throws JsonSchemaException in case this value does not match the given schema
* @throws UnsupportedOperationException in case the given schema is not an interface
* @since 1.0
*/
@TerminalOp(mustBeArray = true)
default void validateEach(Class<?> schema, Rule... rules) {
forEach(e -> JsonValidator.validate(e, schema, rules));
default Stream<Validation.Result> validateEach(
Class<?> schema, Validation.Mode mode, Rule... rules) {
if (mode == Validation.Mode.PROBE) {
return stream().map(e -> JsonValidator.validate(e, schema, mode, rules));
} else {
forEach(e -> JsonValidator.validate(e, schema, mode, rules));
return Stream.empty();
}
}
}
17 changes: 14 additions & 3 deletions src/main/java/org/hisp/dhis/jsontree/JsonAbstractObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,26 @@ default void forEachValue(Consumer<E> action) {
keys().forEach(name -> action.accept(get(name)));
}

/**
* @see #validate(Class, Validation.Mode, Rule...)
* @since 0.11
**/
default void validate(Class<?> schema, Rule... rules) {
validate(schema, Validation.Mode.FAIL_ALL, rules);
}

/**
* @param schema the schema to validate against
* @param mode check, fail fast or give a breakdown of all issues?
* @param rules optional set of {@link Rule}s to check, empty includes all
* @return Validation result if {@link Validation.Mode} is probing (or if
* there were no errors)
* @throws JsonSchemaException in case this value does not match the given schema
* @throws IllegalArgumentException in case the given schema is not an interface
* @since 0.11
* @since 1.9
*/
@TerminalOp(canBeUndefined = true, mustBeObject = true)
default void validate(Class<?> schema, Rule... rules) {
JsonValidator.validate(this, schema, rules);
default Validation.Result validate(Class<?> schema, Validation.Mode mode, Rule... rules) {
return JsonValidator.validate(this, schema, mode, rules);
}
}
65 changes: 47 additions & 18 deletions src/main/java/org/hisp/dhis/jsontree/JsonAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
Expand Down Expand Up @@ -266,7 +267,7 @@ 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.SKIP)
return stream.stream(Index.AUTO_SKIP)
.map(e -> elements.access(e.as(JsonMixed.class), elementType, accessors));
}

Expand Down Expand Up @@ -296,12 +297,18 @@ public static Stream<?> accessAsStream(JsonMixed stream, Type as, JsonAccessors
*
* @param constructor the handle for the canonical constructor
* @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 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
*/
private record NewRecord(
MethodHandle constructor, RecordComponent[] components, MethodHandle of, Type ofArgType) {}
MethodHandle constructor,
RecordComponent[] components,
Object[] defaults,
MethodHandle of,
Type ofArgType) {}

private static final Map<Class<? extends Record>, NewRecord> NEW_RECORD_BY_TYPE =
new ConcurrentHashMap<>();
Expand All @@ -313,14 +320,15 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access
NewRecord newRecord =
NEW_RECORD_BY_TYPE.computeIfAbsent(
type, t -> createRecordFactory(MethodHandles.lookup(), t));
RecordComponent[] components = newRecord.components;
if (!obj.isObject() && !obj.isArray()) {
boolean wrapper = newRecord.components.length == 1;
boolean wrapper = components.length == 1;
MethodHandle fromSingleArg = wrapper ? newRecord.constructor : newRecord.of;
if (fromSingleArg == null)
throw new JsonAccessException(
"JSON does not map to Java record %s, object or array expected"
.formatted(type.getSimpleName()));
Type argType = wrapper ? newRecord.components[0].getGenericType() : newRecord.ofArgType;
Type argType = wrapper ? components[0].getGenericType() : newRecord.ofArgType;
Object arg = accessors.accessor(getRawType(argType)).access(obj, argType, accessors);
try {
return type.cast(fromSingleArg.invokeWithArguments(arg));
Expand All @@ -330,23 +338,27 @@ public static Record accessAsRecord(JsonMixed obj, Type as, JsonAccessors access
}
}

Object[] args = new Object[newRecord.components.length];

Object[] args = new Object[components.length];
Object[] defaults = newRecord.defaults;
boolean hasDefaults = defaults != null;
if (obj.isObject()) {
int i = 0;
for (RecordComponent c : newRecord.components) {
args[i++] =
accessors
.accessor(c.getType())
.access(obj.get(c.getName(), JsonMixed.class), c.getGenericType(), accessors);
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
JsonMixed cValue = obj.get(c.getName());
args[i] =
hasDefaults && cValue.isUndefined()
? defaults[i]
: accessors.accessor(c.getType()).access(cValue, c.getGenericType(), accessors);
}
} else if (obj.isArray()) {
int i = 0;
for (RecordComponent c : newRecord.components)
for (int i = 0; i < components.length; i++) {
RecordComponent c = components[i];
JsonMixed cValue = obj.get(i);
args[i] =
accessors
.accessor(c.getType())
.access(obj.get(i++, JsonMixed.class), c.getGenericType(), accessors);
hasDefaults && cValue.isUndefined()
? defaults[i]
: accessors.accessor(c.getType()).access(cValue, c.getGenericType(), accessors);
}
}
try {
return type.cast(newRecord.constructor.invokeWithArguments(args));
Expand Down Expand Up @@ -399,7 +411,7 @@ private static NewRecord createRecordFactory(
}
}
}
return new NewRecord(canonical, components, ofMethod, ofMethodArgType);
return new NewRecord(canonical, components, defaults(type), ofMethod, ofMethodArgType);
}

private static MethodHandle constructor(
Expand All @@ -419,4 +431,21 @@ private static MethodHandle ofStatic(
return null;
}
}

private static Object[] defaults(Class<? extends Record> type) {
try {
Field defaults = type.getDeclaredField("DEFAULT");
if (defaults.getType() != type) return null;
defaults.setAccessible(true);
Object value = defaults.get(null);
RecordComponent[] components = type.getRecordComponents();
Object[] values = new Object[components.length];
for (int i = 0; i < components.length; i++) {
values[i] = type.getDeclaredMethod(components[i].getName()).invoke(value);
}
return values;
} catch (Exception ex) {
return null;
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/hisp/dhis/jsontree/JsonBoolean.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
*/
@Validation(type = BOOLEAN)
@Validation.Ignore
public interface JsonBoolean extends JsonPrimitive {
public interface JsonBoolean extends JsonValue {

@Override
default JsonBoolean getValue() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/hisp/dhis/jsontree/JsonDate.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.hisp.dhis.jsontree.Validation.NodeType;

/**
* A {@link JsonDate} is a {@link JsonString} with a special format.
Expand All @@ -40,6 +41,7 @@
*
* @author Jan Bernitt
*/
@Validation(type = {NodeType.STRING, NodeType.INTEGER})
@Validation.Ignore
public interface JsonDate extends JsonString {

Expand Down
50 changes: 36 additions & 14 deletions src/main/java/org/hisp/dhis/jsontree/JsonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,29 @@ enum Index {
* multiple methods on a {@link JsonValue} representing the {@link JsonNode} will have the
* node remembered internally after the first lookup.
*/
AUTO
AUTO,

/**
* When the fallback set when constructing the root, for example via {@link
* JsonNode#of(CharSequence, GetListener, Index)} is set to {@link #AUTO} and an operation uses
* {@code AUTO_SKIP} this will {@link #SKIP}, otherwise it will use the fallback set.
*
* <p>This is a good default in tooling build on top of the {@link JsonNode} API. For example in
* pre-processing access like validation, diffing and such. This allows the author creating the
* root note to be control while giving the preference to {@link #SKIP}.
*/
AUTO_SKIP;

/**
* Called on the {@link Index} on the operation level.
*
* @param treeLevel the {@link Index} setting at the tree level
* @return the effective {@link Index} strategy to use
*/
public Index resolve(Index treeLevel) {
if (this == AUTO_SKIP) return treeLevel == AUTO ? SKIP : treeLevel;
return this == AUTO ? treeLevel : this;
}
}

/**
Expand Down Expand Up @@ -350,19 +372,6 @@ default JsonNode getIfExists(@NotNull JsonPath subPath) throws JsonTreeException
throw notAnObject(subPath, this, "getIfExists(JsonPath)");
}

/**
* Size of an array or number of object members.
*
* <p>This is preferable to calling {@link #value()} or {@link #members()} or {@link #elements()}
* when size is only property of interest as it might have better performance.
*
* @return number of elements in an array or number of fields in an object, otherwise undefined
* @throws JsonTreeException when this node in neither an array nor an object
*/
default int size() {
throw notAContainer(this, "size()");
}

/**
* @param subPath relative path to check
* @return true, only if a node exists that the given path relative to this node (this includes a
Expand All @@ -384,6 +393,19 @@ default boolean isEmpty() {
throw notAContainer(this, "isEmpty()");
}

/**
* Size of an array or number of object members.
*
* <p>This is preferable to calling {@link #value()} or {@link #members()} or {@link #elements()}
* when size is only property of interest as it might have better performance.
*
* @return number of elements in an array or number of fields in an object, otherwise undefined
* @throws JsonTreeException when this node in neither an array nor an object
*/
default int size() {
throw notAContainer(this, "size()");
}

/**
* OBS! Only defined when this node is of type {@link JsonNodeType#OBJECT}).
*
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/hisp/dhis/jsontree/JsonNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*/
@Validation(type = NUMBER)
@Validation.Ignore
public interface JsonNumber extends JsonPrimitive {
public interface JsonNumber extends JsonValue {

@Override
default JsonNumber getValue() {
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/org/hisp/dhis/jsontree/JsonObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.lang.reflect.AnnotatedType;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.hisp.dhis.jsontree.JsonNode.Index;
Expand Down Expand Up @@ -66,6 +67,7 @@ default JsonObject getValue() {
* @param jsonName of the property
* @param jsonType the type the property is resolved to internally when calling {@link
* #get(CharSequence, Class)}
* @param types the expected node types based on validation constraints, empty if unknown or unspecified
* @param javaName the name of the java property accessed that caused the JSON property to be
* resolved
* @param javaType the return type of the underlying method that declares the property
Expand All @@ -76,6 +78,7 @@ record Property(
Class<?> in,
Text jsonName,
Class<? extends JsonValue> jsonType,
Set<Validation.NodeType> types,
String javaName,
AnnotatedType javaType,
AnnotatedElement source) {}
Expand Down Expand Up @@ -180,19 +183,18 @@ default Stream<JsonMixed> entries(JsonNode.Index index) {
/**
* Uses the JSON schema validation to check whether this object conforms to the provided type
*
* @param schema a subtype of {@link JsonObject} or {@link Record} to check agsinst
* @param schema a subtype of {@link JsonObject} or {@link Record} to check against
* @param rules optional set of {@link Rule}s to check, empty includes all
* @return true if this is an object is valid against the provided schema
* @since 0.11 (in this form with rules parameter)
*/
@TerminalOp
default boolean isA(Class<?> schema, Rule... rules) {
try {
JsonValidator.validate(this, schema, rules);
return true;
} catch (JsonPathException | JsonTreeException | JsonSchemaException ex) {
return false;
}
if (!exists() || !isObject()) return false;
// Note that this uses PROBE_ALL as that avoid throwing exceptions entirely
Validation.Result result =
JsonValidator.validate(this, schema, Validation.Mode.PROBE_ALL, rules);
return result.errors().isEmpty();
}

/**
Expand All @@ -212,7 +214,7 @@ default boolean isA(Class<?> schema, Rule... rules) {
default <T extends JsonObject> T asA(Class<T> schema, Rule... rules)
throws JsonPathException, JsonTreeException, JsonSchemaException {
T obj = as(schema);
JsonValidator.validate(obj, schema, rules);
JsonValidator.validate(obj, schema, Validation.Mode.FAIL, rules);
return obj;
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/hisp/dhis/jsontree/JsonPointer.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* @since 1.1
* @param value a pointer expression
*/
@Validation(type = STRING, pattern = "(/((~[01])|([^/~]))*)*")
@Validation(type = STRING)
@Validator(value = Validation.RegEx.class, params = @Validation( pattern = "(/((~[01])|([^/~]))*)*"))
public record JsonPointer(@NotNull String value) {

public JsonPointer {
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/org/hisp/dhis/jsontree/JsonPrimitive.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@
import static org.hisp.dhis.jsontree.Validation.NodeType.STRING;

/**
* A common base type for the primitive nodes in a JSON tree.
* The union of all JSON simple or primitive types.
*
* @author Jan Bernitt
*/
@Validation(type = {BOOLEAN, NUMBER, STRING})
@Validation.Ignore
public interface JsonPrimitive extends JsonValue {}
public interface JsonPrimitive extends JsonBoolean, JsonNumber, JsonString {

@Override
default JsonPrimitive getValue() {
return this;
}
}
14 changes: 6 additions & 8 deletions src/main/java/org/hisp/dhis/jsontree/JsonSchemaException.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
/** Thrown when an JSON input does not match its JSON schema description. */
public final class JsonSchemaException extends IllegalArgumentException {

private final transient Info info;
private final transient Validation.Result result;

public record Info(JsonValue value, Class<?> schema, List<Validation.Error> errors) {}

public JsonSchemaException(String message, Info info) {
super(message + toString(info.errors()));
this.info = info;
public JsonSchemaException(String message, Validation.Result result) {
super(message + toString(result.errors()));
this.result = result;
}

public Info getInfo() {
return info;
public Validation.Result getResult() {
return result;
}

private static String toString(List<Validation.Error> errors) {
Expand Down
Loading
Loading