From 45fdaadf47aad31de22791036e321b86ceef3ef3 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:09:23 -0400 Subject: [PATCH 01/14] add adaptation layer SPI --- .../json/schema/spi/JsonAdaptation.java | 82 +++++++++++++++++++ .../everit/json/schema/spi/JsonAdapter.java | 17 ++++ .../json/schema/spi/JsonArrayAdapter.java | 56 +++++++++++++ .../json/schema/spi/JsonObjectAdapter.java | 61 ++++++++++++++ .../everit/json/schema/spi/package-info.java | 9 ++ 5 files changed, 225 insertions(+) create mode 100644 core/src/main/java/org/everit/json/schema/spi/JsonAdaptation.java create mode 100644 core/src/main/java/org/everit/json/schema/spi/JsonAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/spi/JsonArrayAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/spi/JsonObjectAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/spi/package-info.java diff --git a/core/src/main/java/org/everit/json/schema/spi/JsonAdaptation.java b/core/src/main/java/org/everit/json/schema/spi/JsonAdaptation.java new file mode 100644 index 000000000..8ab537a11 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/spi/JsonAdaptation.java @@ -0,0 +1,82 @@ +package org.everit.json.schema.spi; + +/** + * A service that adapts JSON types for use in schema validation. + *

+ * An adaption is responsible for adapting implementation-specific JSON object and array + * structures to instances of the {@link JsonObjectAdapter} and {@link JsonArrayAdapter} types. + * JSON scalar values are adapted to an intrinsic type (String, Boolean, Number). + *

+ * An adaptation can assume that for a given validation operation, exactly one + * adaptation is in use, and that any given {@link JsonAdapter} was produced by + * the single adaptation that is in use. + * + * @param the base type common to the types used in the underlying JSON implementation + */ +public interface JsonAdaptation { + + /** + * Gets the implementation-specific type that represents a JSON array. + * @return array type + */ + Class arrayType(); + + /** + * Gets the implementation-specific type that represents a JSON object. + * @return object type + */ + Class objectType(); + + /** + * Gets the types supported by this implementation. + * @return array of supported types + */ + Class[] supportedTypes(); + + /** + * Given an arbitrary type, tests whether the type is recognized as an adaptable type + * in this adaptation. + *

+ * An implementation does not need to recognize the intrinsic types (String, Boolean, Number) + * but must recognize its own types. + * + * @param type the subject type to test + * @return {@code true} if {@code type} is an adaptable type + */ + boolean isSupportedType(Class type); + + /** + * Given an arbitrary value, tests whether the value is logically equal to null in this + * adaptation. + * @param value the value to test. + * @return {@code true} if {@code value == null} or if {@code value} is equal to the JSON + * null representation supported by this adaptation (if any) + */ + boolean isNull(Object value); + + /** + * Adapts the given value by applying an adaptation function based on the input value. + * @param value the subject value to adapt + * @return the adapted value which must be either + * (1) an intrinsic representation of an implementation-specific JSON scalar value + * (e.g. String, Boolean, Number) + * OR (2) a {@link JsonArrayAdapter} for an implementation-specific JSON array + * OR (3) a {@link JsonObjectAdapter} for an implementation-specific JSON object + * OR (4) the input {@code value} if the value is not a recognized + * implementation-specific type + */ + Object adapt(Object value); + + /** + * Inverts the adaptation function applied to the given value. + * @param value the subject adapted value + * @return the result of inverting any recognized adaptation in {@code value} OR + * {@code value} if no adaptation was recognized. If {@code value} is an instance of + * {@link JsonAdapter} the return value is generally the result of + * {@link JsonAdapter#unwrap()}. If {@code value} has an intrinsic type (String, + * Boolean, Number), the return value may be an implementation-specific representation + * of any of these types. + */ + T invert(Object value); + +} diff --git a/core/src/main/java/org/everit/json/schema/spi/JsonAdapter.java b/core/src/main/java/org/everit/json/schema/spi/JsonAdapter.java new file mode 100644 index 000000000..06f883c59 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/spi/JsonAdapter.java @@ -0,0 +1,17 @@ +package org.everit.json.schema.spi; + +/** + * A adapter for a JSON structure type (array or object). + * + * @param the base type common to the types used in the underlying JSON implementation + */ +public interface JsonAdapter { + + /** + * Gets the implementation-specific representation of the structure instance. + * @return implementation-specific JSON structure representation; generally this is the + * delegate of the adapter + */ + T unwrap(); + +} diff --git a/core/src/main/java/org/everit/json/schema/spi/JsonArrayAdapter.java b/core/src/main/java/org/everit/json/schema/spi/JsonArrayAdapter.java new file mode 100644 index 000000000..aca8c157a --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/spi/JsonArrayAdapter.java @@ -0,0 +1,56 @@ +package org.everit.json.schema.spi; + +import java.util.List; + +/** + * An adapter for a JSON array. + *

+ * This interface represents the contract between a provider's JSON array implementation + * and a validator utilizing JSON Schema. + */ +public interface JsonArrayAdapter extends JsonAdapter { + + /** + * Gets the length of the array. + * @return the number of elements in the array; i.e. the maximum value for the array + * index which can be used {@link #get(int)} to retrieve a value or with + * {@link #put(int, Object)} to store a value + */ + int length(); + + /** + * Retrieves the element of the array at the given index. + *

+ * The validator has no need to retrieve values beyond the bounds of the underlying + * array. Therefore it is reasonable and expected that an implementation throw an + * unchecked exception when {@code index} is outside of the bounds of the array. + * However, it is not a requirement that an exception be thrown in this circumstance. + * + * @param index the index of the element to retrieve + * @return the value of the element or {@code null} if no element + * exists at the specified index + */ + T get(int index); + + /** + * Stores an the element in the array at the given index replacing any existing element + * at the same index. Adaptation from intrinsic types or {@link JsonAdapter} subtypes + * is performed as needed on the input value. + *

+ * The validator has no need to store values beyond the bounds of the underlying + * array. Therefore it is reasonable and expected that an implementation throw an + * unchecked exception when {@code index} is outside of the bounds of the array. + * However, it is not a requirement that an exception be thrown in this circumstance. + * + * @param index index at which to store the element + * @param value the value to store + */ + void put(int index, T value); + + /** + * Adapts this array to the {@link List} interface. + * @return list adaptation + */ + List toList(); + +} diff --git a/core/src/main/java/org/everit/json/schema/spi/JsonObjectAdapter.java b/core/src/main/java/org/everit/json/schema/spi/JsonObjectAdapter.java new file mode 100644 index 000000000..9ef6ac445 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/spi/JsonObjectAdapter.java @@ -0,0 +1,61 @@ +package org.everit.json.schema.spi; + +import java.util.Map; + +/** + * An adapter for a JSON object. + *

+ * This interface represents the contract between a provider's JSON object implementation + * and a validator utilizing JSON Schema. + */ +public interface JsonObjectAdapter extends JsonAdapter { + + /** + * Gets the length of the object. + * @return the number of key-value pairs contained in the object + */ + int length(); + + /** + * Gets an array containing the keys for the key-value pairs contained in the object. + * @return array of keys for values that may be retrieved using {@link #get(String)). + */ + String[] keys(); + + /** + * Tests whether a given key has a corresponding value in the object. + * @param key the subject key + * @return {@code true} if this object has a value that corresponds to {@code key} + */ + boolean has(String key); + + /** + * Retrieves the value associated with a given key from this object, applying adaptation + * to implementation specific types as needed. + *

+ * It is expected that the validator will always test for the presence of a key via + * the {@link #has(String)} method, before attempting to retrieve it. Therefore it is + * reasonable and expected that an implementation throw an unchecked exception when + * {@code key} does not exist in object. However, it is not a requirement that an + * exception be thrown in this circumstance. + * + * @param key the subject key + * @return the value of the element or {@code null} + */ + T get(String key); + + /** + * Replaces any existing value associated with a given key in this object. + * + * @param key the subject key + * @param value the value to store + */ + void put(String key, T value); + + /** + * Adapts the object to the {@link Map} interface. + * @return map adaptation + */ + Map toMap(); + +} diff --git a/core/src/main/java/org/everit/json/schema/spi/package-info.java b/core/src/main/java/org/everit/json/schema/spi/package-info.java new file mode 100644 index 000000000..0e84ef64f --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/spi/package-info.java @@ -0,0 +1,9 @@ +/** + * JSON type service provider interface + *

+ * This package specifies an interface to be implemented for a particular JSON type + * system to make it compatible with the JSON validation components. An implementation + * of this SPI allows validation to performed on JSON objects represented in an + * arbitrary type system. + */ +package org.everit.json.schema.spi; \ No newline at end of file From 399b8999ff89f8e768f5d364df013a8bc1210985 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:09:50 -0400 Subject: [PATCH 02/14] implement the SPI using org.json types --- .../everit/json/schema/JSONAdaptation.java | 72 +++++++++++++++++++ .../everit/json/schema/JSONArrayAdapter.java | 42 +++++++++++ .../everit/json/schema/JSONObjectAdapter.java | 54 ++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 core/src/main/java/org/everit/json/schema/JSONAdaptation.java create mode 100644 core/src/main/java/org/everit/json/schema/JSONArrayAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/JSONObjectAdapter.java diff --git a/core/src/main/java/org/everit/json/schema/JSONAdaptation.java b/core/src/main/java/org/everit/json/schema/JSONAdaptation.java new file mode 100644 index 000000000..0f5fbcce2 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/JSONAdaptation.java @@ -0,0 +1,72 @@ +package org.everit.json.schema; + +import org.everit.json.schema.spi.JsonAdaptation; +import org.everit.json.schema.spi.JsonAdapter; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.stream.Stream; + +/** + * A {@link JsonAdaptation} that uses {@link JSONArray} as the array type and + * {@link JSONObject} as the object type. + * + */ +class JSONAdaptation implements JsonAdaptation { + + private static final Class[] SUPPORTED_TYPES = { + JSONArray.class, + JSONObject.class, + JSONObject.NULL.getClass() + }; + + @Override + public Class arrayType() { + return JSONArray.class; + } + + @Override + public Class objectType() { + return JSONObject.class; + } + + @Override + public Class[] supportedTypes() { + return SUPPORTED_TYPES; + } + + @Override + public boolean isSupportedType(Class type) { + return Stream.of(SUPPORTED_TYPES).anyMatch(t -> t.isAssignableFrom(type)); + } + + @Override + public boolean isNull(Object value) { + return value == null || JSONObject.NULL.equals(value); + } + + @Override + public Object adapt(Object value) { + if (JSONObject.NULL.equals(value)) { + return null; + } else if (value instanceof JSONArray) { + return new JSONArrayAdapter((JSONArray) value); + } else if (value instanceof JSONObject) { + return new JSONObjectAdapter((JSONObject) value); + } else { + return value; + } + } + + @Override + public Object invert(Object value) { + if (value == null) { + return JSONObject.NULL; + } else if (value instanceof JsonAdapter) { + return ((JsonAdapter) value).unwrap(); + } else { + return value; + } + } + +} diff --git a/core/src/main/java/org/everit/json/schema/JSONArrayAdapter.java b/core/src/main/java/org/everit/json/schema/JSONArrayAdapter.java new file mode 100644 index 000000000..e6e22aa04 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/JSONArrayAdapter.java @@ -0,0 +1,42 @@ +package org.everit.json.schema; + +import org.everit.json.schema.spi.JsonArrayAdapter; +import org.json.JSONArray; + +import java.util.List; + +/** + * A {@link JsonArrayAdapter} that delegates to a {@link JSONArray}. + */ +class JSONArrayAdapter implements JsonArrayAdapter { + + private final JSONArray delegate; + + JSONArrayAdapter(JSONArray delegate) { + this.delegate = delegate; + } + + @Override + public Object unwrap() { + return delegate; + } + + public Object get(int index) { + return delegate.get(index); + } + + public int length() { + return delegate.length(); + } + + @Override + public void put(int index, Object value) { + delegate.put(index, value); + } + + @Override + public List toList() { + return delegate.toList(); + } + +} diff --git a/core/src/main/java/org/everit/json/schema/JSONObjectAdapter.java b/core/src/main/java/org/everit/json/schema/JSONObjectAdapter.java new file mode 100644 index 000000000..890a7f307 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/JSONObjectAdapter.java @@ -0,0 +1,54 @@ +package org.everit.json.schema; + +import org.everit.json.schema.spi.JsonObjectAdapter; +import org.json.JSONObject; + +import java.util.Map; + +/** + * A {@link JsonObjectAdapter} that delegates to a {@link JSONObject}. + */ +class JSONObjectAdapter implements JsonObjectAdapter { + + private final JSONObject delegate; + + JSONObjectAdapter(JSONObject delegate) { + this.delegate = delegate; + } + + @Override + public Object unwrap() { + return delegate; + } + + @Override + public int length() { + return delegate.length(); + } + + @Override + public String[] keys() { + return JSONObject.getNames(delegate); + } + + @Override + public boolean has(String key) { + return delegate.has(key); + } + + @Override + public Object get(String key) { + return delegate.get(key); + } + + @Override + public void put(String key, Object value) { + delegate.put(key, value); + } + + @Override + public Map toMap() { + return delegate.toMap(); + } + +} From 6aa815226012154d7ee38f757db43822c24d6601 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:10:36 -0400 Subject: [PATCH 03/14] weave the JSON type adaptation into the validation components --- .../schema/ArraySchemaValidatingVisitor.java | 18 +++-- .../org/everit/json/schema/EnumSchema.java | 12 +++- .../schema/NumberSchemaValidatingVisitor.java | 9 ++- .../everit/json/schema/ObjectComparator.java | 42 ++++++++---- .../schema/ObjectSchemaValidatingVisitor.java | 34 ++++++---- .../schema/StringSchemaValidatingVisitor.java | 8 ++- .../everit/json/schema/ValidatingVisitor.java | 65 ++++++++++--------- .../org/everit/json/schema/Validator.java | 18 ++++- .../json/schema/ValidatingVisitorTest.java | 20 +++--- 9 files changed, 145 insertions(+), 81 deletions(-) diff --git a/core/src/main/java/org/everit/json/schema/ArraySchemaValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/ArraySchemaValidatingVisitor.java index bf92e06cc..a3f15b112 100644 --- a/core/src/main/java/org/everit/json/schema/ArraySchemaValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/ArraySchemaValidatingVisitor.java @@ -1,7 +1,7 @@ package org.everit.json.schema; -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; +import org.everit.json.schema.spi.JsonAdaptation; +import org.everit.json.schema.spi.JsonArrayAdapter; import java.util.ArrayList; import java.util.Collection; @@ -10,7 +10,8 @@ import java.util.function.IntFunction; import java.util.stream.IntStream; -import org.json.JSONArray; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; class ArraySchemaValidatingVisitor extends Visitor { @@ -18,20 +19,23 @@ class ArraySchemaValidatingVisitor extends Visitor { private final ValidatingVisitor owner; - private JSONArray arraySubject; + private final JsonAdaptation jsonAdaptation; + + private JsonArrayAdapter arraySubject; private ArraySchema arraySchema; private int subjectLength; - public ArraySchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { + public ArraySchemaValidatingVisitor(Object subject, ValidatingVisitor owner, JsonAdaptation jsonAdaptation) { this.subject = subject; this.owner = requireNonNull(owner, "owner cannot be null"); + this.jsonAdaptation = jsonAdaptation; } @Override void visitArraySchema(ArraySchema arraySchema) { - if (owner.passesTypeCheck(JSONArray.class, arraySchema.requiresArray(), arraySchema.isNullable())) { - this.arraySubject = (JSONArray) subject; + if (owner.passesTypeCheck(jsonAdaptation.arrayType(), arraySchema.requiresArray(), arraySchema.isNullable())) { + this.arraySubject = (JsonArrayAdapter) jsonAdaptation.adapt(subject); this.subjectLength = arraySubject.length(); this.arraySchema = arraySchema; super.visitArraySchema(arraySchema); diff --git a/core/src/main/java/org/everit/json/schema/EnumSchema.java b/core/src/main/java/org/everit/json/schema/EnumSchema.java index 769dc15c9..f665d8eb6 100644 --- a/core/src/main/java/org/everit/json/schema/EnumSchema.java +++ b/core/src/main/java/org/everit/json/schema/EnumSchema.java @@ -9,6 +9,8 @@ import java.util.Set; import java.util.stream.Collectors; import org.everit.json.schema.internal.JSONPrinter; +import org.everit.json.schema.spi.JsonArrayAdapter; +import org.everit.json.schema.spi.JsonObjectAdapter; import org.json.JSONArray; import org.json.JSONObject; @@ -18,11 +20,15 @@ public class EnumSchema extends Schema { static Object toJavaValue(Object orig) { - if (orig instanceof JSONArray) { + if (orig instanceof JsonArrayAdapter) { + return ((JsonArrayAdapter) orig).toList(); + } else if (orig instanceof JsonObjectAdapter) { + return ((JsonObjectAdapter) orig).toMap(); + } else if (orig instanceof JSONArray) { // recognize this type to support test cases that don't adapt it return ((JSONArray) orig).toList(); - } else if (orig instanceof JSONObject) { + } else if (orig instanceof JSONObject) { // recognize this type to support test cases that don't adapt it return ((JSONObject) orig).toMap(); - } else if (orig == JSONObject.NULL) { + } else if (orig == JSONObject.NULL) { // recognize this value to support test cases that don't adapt it return null; } else { return orig; diff --git a/core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java index 937091a32..f1ad1601e 100644 --- a/core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java @@ -1,5 +1,7 @@ package org.everit.json.schema; +import org.everit.json.schema.spi.JsonAdaptation; + import static java.lang.String.format; import java.math.BigDecimal; @@ -18,15 +20,18 @@ class NumberSchemaValidatingVisitor extends Visitor { private final ValidatingVisitor owner; + private final JsonAdaptation jsonAdaptation; + private boolean exclusiveMinimum; private boolean exclusiveMaximum; private double numberSubject; - NumberSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { + NumberSchemaValidatingVisitor(Object subject, ValidatingVisitor owner, JsonAdaptation jsonAdaptation) { this.subject = subject; this.owner= owner; + this.jsonAdaptation = jsonAdaptation; } @Override void visitNumberSchema(NumberSchema numberSchema) { @@ -34,7 +39,7 @@ class NumberSchemaValidatingVisitor extends Visitor { if (!INTEGRAL_TYPES.contains(subject.getClass()) && numberSchema.requiresInteger()) { owner.failure(Integer.class, subject); } else { - this.numberSubject = ((Number) subject).doubleValue(); + this.numberSubject = ((Number) jsonAdaptation.adapt(subject)).doubleValue(); super.visitNumberSchema(numberSchema); } } diff --git a/core/src/main/java/org/everit/json/schema/ObjectComparator.java b/core/src/main/java/org/everit/json/schema/ObjectComparator.java index e8984e7d6..13e8b87f7 100644 --- a/core/src/main/java/org/everit/json/schema/ObjectComparator.java +++ b/core/src/main/java/org/everit/json/schema/ObjectComparator.java @@ -1,13 +1,17 @@ package org.everit.json.schema; -import java.util.Arrays; -import java.util.Objects; +import java.util.*; +import org.everit.json.schema.spi.JsonArrayAdapter; +import org.everit.json.schema.spi.JsonObjectAdapter; import org.json.JSONArray; import org.json.JSONObject; /** - * Deep-equals implementation on primitive wrappers, {@link JSONObject} and {@link JSONArray}. + * Deep-equals implementation on primitive wrappers, array and object adapters, and + * {@link JSONObject} and {@link JSONArray}. {@link JSONArray} and {@link JSONObject} are + * included in order to support test cases without the need to first wrap in an adapter. + * */ public final class ObjectComparator { @@ -21,21 +25,32 @@ public final class ObjectComparator { * @return {@code true} if the two objects are equal, {@code false} otherwise */ public static boolean deepEquals(Object obj1, Object obj2) { - if (obj1 instanceof JSONArray) { - if (!(obj2 instanceof JSONArray)) { + if (obj1 instanceof JsonArrayAdapter) { + if (!(obj2 instanceof JsonArrayAdapter)) { return false; } - return deepEqualArrays((JSONArray) obj1, (JSONArray) obj2); - } else if (obj1 instanceof JSONObject) { - if (!(obj2 instanceof JSONObject)) { + return deepEqualArrays((JsonArrayAdapter) obj1, (JsonArrayAdapter) obj2); + } else if (obj1 instanceof JsonObjectAdapter) { + if (!(obj2 instanceof JsonObjectAdapter)) { return false; } - return deepEqualObjects((JSONObject) obj1, (JSONObject) obj2); + return deepEqualObjects((JsonObjectAdapter) obj1, (JsonObjectAdapter) obj2); + } else if (obj1 instanceof JSONArray) { // continue to recognize the JSONArray type to support + if (!(obj2 instanceof JSONArray)) { // test cases that don't adapt org.json types + return false; + } + return deepEqualArrays(new JSONArrayAdapter((JSONArray) obj1), new JSONArrayAdapter((JSONArray) obj2)); + } else if (obj1 instanceof JSONObject) { // continue to recognize the JSONArray type to support + if (!(obj2 instanceof JSONObject)) { // test cases that don't adapt org.json types + return false; + } + return deepEqualObjects(new JSONObjectAdapter((JSONObject) obj1), new JSONObjectAdapter((JSONObject) obj2)); } + return Objects.equals(obj1, obj2); } - private static boolean deepEqualArrays(JSONArray arr1, JSONArray arr2) { + private static boolean deepEqualArrays(JsonArrayAdapter arr1, JsonArrayAdapter arr2) { if (arr1.length() != arr2.length()) { return false; } @@ -47,8 +62,8 @@ private static boolean deepEqualArrays(JSONArray arr1, JSONArray arr2) { return true; } - private static String[] sortedNamesOf(JSONObject obj) { - String[] raw = JSONObject.getNames(obj); + private static String[] sortedNamesOf(JsonObjectAdapter obj) { + String[] raw = obj.keys(); if (raw == null) { return null; } @@ -56,7 +71,8 @@ private static String[] sortedNamesOf(JSONObject obj) { return raw; } - private static boolean deepEqualObjects(JSONObject jsonObj1, JSONObject jsonObj2) { + private static boolean deepEqualObjects(JsonObjectAdapter jsonObj1, + JsonObjectAdapter jsonObj2) { String[] obj1Names = sortedNamesOf(jsonObj1); if (!Arrays.equals(obj1Names, sortedNamesOf(jsonObj2))) { return false; diff --git a/core/src/main/java/org/everit/json/schema/ObjectSchemaValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/ObjectSchemaValidatingVisitor.java index 32647c43e..9a4a6cba0 100644 --- a/core/src/main/java/org/everit/json/schema/ObjectSchemaValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/ObjectSchemaValidatingVisitor.java @@ -8,13 +8,14 @@ import java.util.Set; import org.everit.json.schema.regexp.Regexp; -import org.json.JSONObject; +import org.everit.json.schema.spi.JsonAdaptation; +import org.everit.json.schema.spi.JsonObjectAdapter; class ObjectSchemaValidatingVisitor extends Visitor { private final Object subject; - private JSONObject objSubject; + private JsonObjectAdapter objSubject; private ObjectSchema schema; @@ -22,14 +23,18 @@ class ObjectSchemaValidatingVisitor extends Visitor { private final ValidatingVisitor owner; - public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { + private final JsonAdaptation jsonAdaptation; + + public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner, JsonAdaptation jsonAdaptation) { this.subject = requireNonNull(subject, "subject cannot be null"); this.owner = requireNonNull(owner, "owner cannot be null"); + this.jsonAdaptation = jsonAdaptation; } @Override void visitObjectSchema(ObjectSchema objectSchema) { - if (owner.passesTypeCheck(JSONObject.class, objectSchema.requiresObject(), objectSchema.isNullable())) { - objSubject = (JSONObject) subject; + if (owner.passesTypeCheck(jsonAdaptation.objectType(), objectSchema.requiresObject(), + objectSchema.isNullable())) { + objSubject = (JsonObjectAdapter) jsonAdaptation.adapt(subject); objectSize = objSubject.length(); this.schema = objectSchema; super.visitObjectSchema(objectSchema); @@ -44,7 +49,7 @@ public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { @Override void visitPropertyNameSchema(Schema propertyNameSchema) { if (propertyNameSchema != null) { - String[] names = JSONObject.getNames(objSubject); + String[] names = objSubject.keys(); if (names == null || names.length == 0) { return; } @@ -105,7 +110,7 @@ public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { } private List getAdditionalProperties() { - String[] names = JSONObject.getNames(objSubject); + String[] names = objSubject.keys(); if (names == null) { return new ArrayList<>(); } else { @@ -129,7 +134,7 @@ private boolean matchesAnyPattern(String key) { } @Override void visitPatternPropertySchema(Regexp propertyNamePattern, Schema schema) { - String[] propNames = JSONObject.getNames(objSubject); + String[] propNames = objSubject.keys(); if (propNames == null || propNames.length == 0) { return; } @@ -152,14 +157,17 @@ private boolean matchesAnyPattern(String key) { } } - @Override void visitPropertySchema(String properyName, Schema schema) { - if (objSubject.has(properyName)) { - ValidationException failure = owner.getFailureOfSchema(schema, objSubject.get(properyName)); + @SuppressWarnings("unchecked") + @Override void visitPropertySchema(String propertyName, Schema schema) { + if (objSubject.has(propertyName)) { + ValidationException failure = owner.getFailureOfSchema(schema, objSubject.get(propertyName)); if (failure != null) { - owner.failure(failure.prepend(properyName)); + owner.failure(failure.prepend(propertyName)); } } else if (schema.hasDefaultValue()) { - objSubject.put(properyName, schema.getDefaultValue()); + // we're using the raw type here under the assumption that the object adapter and + // the adaptation that produced it are using the same type T. + objSubject.put(propertyName, jsonAdaptation.invert(schema.getDefaultValue())); } } } diff --git a/core/src/main/java/org/everit/json/schema/StringSchemaValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/StringSchemaValidatingVisitor.java index c32ebbe65..289f8de74 100644 --- a/core/src/main/java/org/everit/json/schema/StringSchemaValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/StringSchemaValidatingVisitor.java @@ -6,6 +6,7 @@ import java.util.Optional; import org.everit.json.schema.regexp.Regexp; +import org.everit.json.schema.spi.JsonAdaptation; public class StringSchemaValidatingVisitor extends Visitor { @@ -17,14 +18,17 @@ public class StringSchemaValidatingVisitor extends Visitor { private final ValidatingVisitor owner; - public StringSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { + private final JsonAdaptation jsonAdaptation; + + public StringSchemaValidatingVisitor(Object subject, ValidatingVisitor owner, JsonAdaptation jsonAdaptation) { this.subject = subject; this.owner = requireNonNull(owner, "failureReporter cannot be null"); + this.jsonAdaptation = requireNonNull(jsonAdaptation, "jsonAdaptation cannot be null"); } @Override void visitStringSchema(StringSchema stringSchema) { if (owner.passesTypeCheck(String.class, stringSchema.requireString(), stringSchema.isNullable())) { - stringSubject = (String) subject; + stringSubject = (String) jsonAdaptation.adapt(subject); stringLength = stringSubject.codePointCount(0, stringSubject.length()); super.visitStringSchema(stringSchema); } diff --git a/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java index 8795b15ca..b022daa86 100644 --- a/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java @@ -1,35 +1,27 @@ package org.everit.json.schema; -import static java.lang.String.format; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; -import static java.util.stream.Collectors.joining; -import static org.everit.json.schema.EnumSchema.toJavaValue; +import org.everit.json.schema.spi.JsonAdaptation; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Stream; -import org.json.JSONArray; -import org.json.JSONObject; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.joining; +import static org.everit.json.schema.EnumSchema.toJavaValue; class ValidatingVisitor extends Visitor { - private static final List> VALIDATED_TYPES = unmodifiableList(asList( + private static final List> INTRINSIC_TYPES = unmodifiableList(asList( Number.class, String.class, - Boolean.class, - JSONObject.class, - JSONArray.class, - JSONObject.NULL.getClass() + Boolean.class )); - static final String TYPE_FAILURE_MSG = "subject is an instance of non-handled type %s. Should be one of " - + VALIDATED_TYPES.stream().map(Class::getSimpleName).collect(joining(", ")); - - private static boolean isNull(Object obj) { - return obj == null || JSONObject.NULL.equals(obj); - } + static final String TYPE_FAILURE_MSG = "subject is an instance of non-handled type %s. Should be one of %s."; protected Object subject; @@ -37,6 +29,12 @@ private static boolean isNull(Object obj) { private final ReadWriteValidator readWriteValidator; + private final JsonAdaptation jsonAdaptation; + + private boolean isNull(Object obj) { + return obj == null || jsonAdaptation.isNull(obj); + } + @Override void visit(Schema schema) { if (schema.isNullable() == Boolean.FALSE && isNull(subject)) { @@ -46,23 +44,29 @@ void visit(Schema schema) { super.visit(schema); } - ValidatingVisitor(Object subject, ValidationFailureReporter failureReporter, ReadWriteValidator readWriteValidator) { - if (subject != null && !VALIDATED_TYPES.stream().anyMatch(type -> type.isAssignableFrom(subject.getClass()))) { - throw new IllegalArgumentException(format(TYPE_FAILURE_MSG, subject.getClass().getSimpleName())); + ValidatingVisitor(Object subject, ValidationFailureReporter failureReporter, + ReadWriteValidator readWriteValidator, JsonAdaptation jsonAdaptation) { + if (subject != null + && !INTRINSIC_TYPES.stream().anyMatch(type -> type.isAssignableFrom(subject.getClass())) + && !jsonAdaptation.isSupportedType(subject.getClass())) { + throw new IllegalArgumentException(format(TYPE_FAILURE_MSG, subject.getClass().getSimpleName(), + Stream.concat(INTRINSIC_TYPES.stream(), Stream.of(jsonAdaptation.supportedTypes())) + .map(Class::getSimpleName).collect(joining(", ")))); } this.subject = subject; + this.jsonAdaptation = jsonAdaptation; this.failureReporter = failureReporter; this.readWriteValidator = readWriteValidator; } @Override void visitNumberSchema(NumberSchema numberSchema) { - numberSchema.accept(new NumberSchemaValidatingVisitor(subject, this)); + numberSchema.accept(new NumberSchemaValidatingVisitor(subject, this, jsonAdaptation)); } @Override void visitArraySchema(ArraySchema arraySchema) { - arraySchema.accept(new ArraySchemaValidatingVisitor(subject, this)); + arraySchema.accept(new ArraySchemaValidatingVisitor(subject, this, jsonAdaptation)); } @Override @@ -74,7 +78,7 @@ void visitBooleanSchema(BooleanSchema schema) { @Override void visitNullSchema(NullSchema nullSchema) { - if (!(subject == null || subject == JSONObject.NULL)) { + if (!(subject == null || jsonAdaptation.isNull(subject))) { failureReporter.failure("expected: null, found: " + subject.getClass().getSimpleName(), "type"); } } @@ -84,7 +88,7 @@ void visitConstSchema(ConstSchema constSchema) { if (isNull(subject) && isNull(constSchema.getPermittedValue())) { return; } - Object effectiveSubject = toJavaValue(subject); + Object effectiveSubject = toJavaValue(jsonAdaptation.adapt(subject)); if (!ObjectComparator.deepEquals(effectiveSubject, constSchema.getPermittedValue())) { failureReporter.failure("", "const"); } @@ -92,7 +96,8 @@ void visitConstSchema(ConstSchema constSchema) { @Override void visitEnumSchema(EnumSchema enumSchema) { - Object effectiveSubject = toJavaValue(subject); + if (jsonAdaptation.isNull(subject)) return; + Object effectiveSubject = toJavaValue(jsonAdaptation.adapt(subject)); for (Object possibleValue : enumSchema.getPossibleValues()) { if (ObjectComparator.deepEquals(possibleValue, effectiveSubject)) { return; @@ -129,12 +134,12 @@ void visitReferenceSchema(ReferenceSchema referenceSchema) { @Override void visitObjectSchema(ObjectSchema objectSchema) { - objectSchema.accept(new ObjectSchemaValidatingVisitor(subject, this)); + objectSchema.accept(new ObjectSchemaValidatingVisitor(subject, this, jsonAdaptation)); } @Override void visitStringSchema(StringSchema stringSchema) { - stringSchema.accept(new StringSchemaValidatingVisitor(subject, this)); + stringSchema.accept(new StringSchemaValidatingVisitor(subject, this, jsonAdaptation)); } @Override @@ -168,7 +173,9 @@ void visitConditionalSchema(ConditionalSchema conditionalSchema) { ValidationException getFailureOfSchema(Schema schema, Object input) { Object origSubject = this.subject; - this.subject = input; + // TODO: performance could be improved by revisiting test cases that break when + // the adaptation value is not inverted here + this.subject = jsonAdaptation.invert(input); ValidationException rval = failureReporter.inContextOfSchema(schema, () -> visit(schema)); this.subject = origSubject; return rval; diff --git a/core/src/main/java/org/everit/json/schema/Validator.java b/core/src/main/java/org/everit/json/schema/Validator.java index b780b8995..5238e206c 100644 --- a/core/src/main/java/org/everit/json/schema/Validator.java +++ b/core/src/main/java/org/everit/json/schema/Validator.java @@ -1,5 +1,7 @@ package org.everit.json.schema; +import org.everit.json.schema.spi.JsonAdaptation; + import java.util.function.BiFunction; public interface Validator { @@ -10,6 +12,8 @@ class ValidatorBuilder { private ReadWriteContext readWriteContext; + private JsonAdaptation jsonAdaptation = new JSONAdaptation(); + public ValidatorBuilder failEarly() { this.failEarly = true; return this; @@ -20,8 +24,13 @@ public ValidatorBuilder readWriteContext(ReadWriteContext readWriteContext) { return this; } + public ValidatorBuilder jsonAdaptation(JsonAdaptation jsonAdaptation) { + this.jsonAdaptation = jsonAdaptation; + return this; + } + public Validator build() { - return new DefaultValidator(failEarly, readWriteContext); + return new DefaultValidator(failEarly, readWriteContext, jsonAdaptation); } } @@ -41,15 +50,18 @@ class DefaultValidator implements Validator { private final ReadWriteContext readWriteContext; - DefaultValidator(boolean failEarly, ReadWriteContext readWriteContext) { + private final JsonAdaptation jsonAdaptation; + + DefaultValidator(boolean failEarly, ReadWriteContext readWriteContext, JsonAdaptation jsonAdaptation) { this.failEarly = failEarly; this.readWriteContext = readWriteContext; + this.jsonAdaptation = jsonAdaptation; } @Override public void performValidation(Schema schema, Object input) { ValidationFailureReporter failureReporter = createFailureReporter(schema); ReadWriteValidator readWriteValidator = ReadWriteValidator.createForContext(readWriteContext, failureReporter); - ValidatingVisitor visitor = new ValidatingVisitor(input, failureReporter, readWriteValidator); + ValidatingVisitor visitor = new ValidatingVisitor(input, failureReporter, readWriteValidator, jsonAdaptation); visitor.visit(schema); visitor.failIfErrorFound(); } diff --git a/core/src/test/java/org/everit/json/schema/ValidatingVisitorTest.java b/core/src/test/java/org/everit/json/schema/ValidatingVisitorTest.java index 7d809197c..bd2fd626a 100644 --- a/core/src/test/java/org/everit/json/schema/ValidatingVisitorTest.java +++ b/core/src/test/java/org/everit/json/schema/ValidatingVisitorTest.java @@ -22,6 +22,8 @@ @RunWith(JUnitParamsRunner.class) public class ValidatingVisitorTest { + private static final JSONAdaptation ORG_JSON_ADAPTATION = new JSONAdaptation(); + private ValidationFailureReporter reporter; @Before @@ -31,49 +33,49 @@ public void before() { @Test public void passesTypeCheck_otherType_noRequires() { - ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null); + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, false, null)); verifyZeroInteractions(reporter); } @Test public void passesTypeCheck_otherType_requires() { - ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null); + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, true, null)); verify(reporter).failure(JSONObject.class, "string"); } @Test public void passesTypeCheck_otherType_nullPermitted_nullObject() { - ValidatingVisitor subject = new ValidatingVisitor(JSONObject.NULL, reporter, null); + ValidatingVisitor subject = new ValidatingVisitor(JSONObject.NULL, reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, true, Boolean.TRUE)); verifyZeroInteractions(reporter); } @Test public void passesTypeCheck_otherType_nullPermitted_nullReference() { - ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null); + ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, true, Boolean.TRUE)); verifyZeroInteractions(reporter); } @Test public void passesTypeCheck_nullPermitted_nonNullValue() { - ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null); + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, true, Boolean.TRUE)); verify(reporter).failure(JSONObject.class, "string"); } @Test public void passesTypeCheck_requiresType_nullableIsNull() { - ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null); + ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null, ORG_JSON_ADAPTATION); assertFalse(subject.passesTypeCheck(JSONObject.class, true, null)); verify(reporter).failure(JSONObject.class, null); } @Test public void passesTypeCheck_sameType() { - ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null); + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, ORG_JSON_ADAPTATION); assertTrue(subject.passesTypeCheck(String.class, true, Boolean.TRUE)); verifyZeroInteractions(reporter); } @@ -105,13 +107,13 @@ public Object[] notPermittedTypes() { @Test @Parameters(method = "permittedTypes") public void permittedTypeSuccess(Object subject) { - new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE); + new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE, ORG_JSON_ADAPTATION); } @Test(expected = IllegalArgumentException.class) @Parameters(method = "notPermittedTypes") public void notPermittedTypeFailure(Object subject) { - new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE); + new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE, ORG_JSON_ADAPTATION); } } From e74ffa2dd8a9480ee9d0607c1bbddb8729c19704 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:18:39 -0400 Subject: [PATCH 04/14] implement the adaptation SPI using JSON-P types --- core/pom.xml | 6 + .../javax/json/JavaxJsonAdaptation.java | 218 ++++++++++++++++++ .../javax/json/JavaxJsonArrayAdapter.java | 46 ++++ .../javax/json/JavaxJsonObjectAdapter.java | 56 +++++ .../schema/javax/json/Jsr353Adaptation.java | 47 ++++ .../schema/javax/json/Jsr374Adaptation.java | 43 ++++ .../json/schema/javax/json/package-info.java | 4 + .../JavaxJsonValidatingVisitorTest.java | 154 +++++++++++++ 8 files changed, 574 insertions(+) create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/Jsr374Adaptation.java create mode 100644 core/src/main/java/org/everit/json/schema/javax/json/package-info.java create mode 100644 core/src/test/java/org/everit/json/schema/JavaxJsonValidatingVisitorTest.java diff --git a/core/pom.xml b/core/pom.xml index cb7b42d89..0054cbcb5 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -139,6 +139,12 @@ json 20180130 + + org.glassfish + javax.json + 1.1.2 + true + com.google.guava guava diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java new file mode 100644 index 000000000..6cf4dd6f5 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java @@ -0,0 +1,218 @@ +package org.everit.json.schema.javax.json; + +import org.everit.json.schema.spi.JsonAdaptation; +import org.everit.json.schema.spi.JsonAdapter; + +import javax.json.*; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * A {@link JsonAdaptation} for the standard Java JSON types in the {@code javax.json} + * package. + *

+ * The current specification for these types is part of the JSON-P specification + * (JSR-374), which is part of the Java EE 8 platform. An earlier version of the JSON-P + * specification (JSR-353) was used in the Java EE 7 platform. + *

+ * Because the later specification provides some improvements to the API needed to support + * these types with JSON schema validation, we want to use the later version where possible. + * However, most Java EE 7 containers will have the older JSR-353 API. + *

+ * Using the {@link #newInstance()} method, a user of this adaptation can get an instance + * that uses the latest version of the JSON-P that is available at runtime. + */ +public abstract class JavaxJsonAdaptation implements JsonAdaptation { + + private static final String JSR_374_ADAPTATION = "Jsr374Adaptation"; + private static final String JSR_353_ADAPTATION = "Jsr353Adaptation"; + + @Override + public Class arrayType() { + return JsonArray.class; + } + + @Override + public Class objectType() { + return JsonObject.class; + } + + @Override + public Class[] supportedTypes() { + return new Class[] { JsonValue.class }; + } + + /** + * Creates a new instance of an adaptation for the standard JSON types using the + * latest version of the JSON-P implementation that is available via either the + * thread context class loader (if one is set on the calling thread) or from the + * same class loader that loaded this class. + * + * @return adaptation instance for JSON-P data types + * @throws RuntimeException if the adaptation class cannot be instantiated + */ + public static JavaxJsonAdaptation newInstance() { + if (Thread.currentThread().getContextClassLoader() != null) { + return newInstance(Thread.currentThread().getContextClassLoader()); + } + return newInstance(JavaxJsonAdaptation.class.getClassLoader()); + } + + /** + * Creates a new instance of an adaptation for the standard JSON types using the + * latest version of the JSON-P implementation that is available on the specified + * class loader. + *

+ * This method may be use in dynamic modular runtime environments such as those + * provided by OSGi. + * + * @param classLoader the class loader to use to find the JSON-P API and + * implementation classes + * @return adaptation instance for JSON-P data types + * @throws RuntimeException if the adaptation class cannot be instantiated + */ + public static JavaxJsonAdaptation newInstance(ClassLoader classLoader) { + try { + Json.class.getMethod("createValue", String.class); + return newInstance(classLoader, JSR_374_ADAPTATION); + } catch (NoSuchMethodException ex) { + return newInstance(classLoader, JSR_353_ADAPTATION); + } + } + + /** + * Creates a new adaptation instance using the given class loader to load a + * specific provider name. + * @param classLoader source class loader for the JSON-P types + * @param providerName name of the {@link JavaxJsonAdaptation} to load + * @return adaptation instance + * @throws RuntimeException an instance of the specified type cannot be instantiated + */ + private static JavaxJsonAdaptation newInstance(ClassLoader classLoader, + String providerName) { + try { + return (JavaxJsonAdaptation) classLoader.loadClass( + providerClassName(providerName)).newInstance(); + } + catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Constructs the fully-qualified class name for a given named provider class. + *

+ * It is assumed that all providers are subtypes of this abstract base type and + * are located in the same package. + * + * @param providerName provider class name + * @return fully qualified class name + */ + private static String providerClassName(String providerName) { + return JavaxJsonAdaptation.class.getPackage().getName() + "." + providerName; + } + + @Override + public boolean isSupportedType(Class type) { + return JsonValue.class.isAssignableFrom(type); + } + + @Override + public boolean isNull(Object value) { + return value == null || value == JsonValue.NULL; + } + + @Override + public Object adapt(Object value) { + if (value == JsonValue.TRUE) { + return true; + } else if (value == JsonValue.FALSE) { + return false; + } else if (value instanceof JsonString) { + return ((JsonString) value).getString(); + } else if (value instanceof JsonNumber) { + return ((JsonNumber) value).bigDecimalValue(); + } else if (value instanceof JsonArray) { + return new JavaxJsonArrayAdapter((JsonArray) value); + } else if (value instanceof JsonObject) { + return new JavaxJsonObjectAdapter((JsonObject) value); + } else { + return value; + } + } + + @Override + public JsonValue invert(Object value) { + if (value == null) { + return JsonValue.NULL; + } + if (value instanceof JsonAdapter) { + return (JsonValue) ((JsonAdapter) value).unwrap(); + } else if (value instanceof String) { + return createValue((String) value); + } else if (value instanceof Boolean) { + return ((boolean) value) ? JsonValue.TRUE : JsonValue.FALSE; + } else if (value instanceof Integer || value instanceof Byte || value instanceof Short) { + return createValue((int) value); + } else if (value instanceof Long) { + return createValue((long) value); + } else if (value instanceof Double || value instanceof Float) { + return createValue((double) value); + } else if (value instanceof BigInteger) { + return createValue((BigInteger) value); + } else if (value instanceof BigDecimal) { + return createValue((BigDecimal) value); + } else { + throw new AssertionError("unrecognized intrinsic type"); + } + } + + /** + * Creates a {@link JsonValue} representing the given string using a provider-specific + * mechanism. + * @param value the subject string value + * @return JSON value instance + */ + abstract JsonValue createValue(String value); + + /** + * Creates a {@link JsonValue} representing the given integer using a provider-specific + * mechanism. + * @param value the subject integer value + * @return JSON value instance + */ + abstract JsonValue createValue(int value); + + /** + * Creates a {@link JsonValue} representing the given long using a provider-specific + * mechanism. + * @param value the subject long value + * @return JSON value instance + */ + abstract JsonValue createValue(long value); + + /** + * Creates a {@link JsonValue} representing the given double using a provider-specific + * mechanism. + * @param value the subject double value + * @return JSON value instance + */ + abstract JsonValue createValue(double value); + + /** + * Creates a {@link JsonValue} representing the given big integer using a + * provider-specific mechanism. + * @param value the subject big integer value + * @return JSON value instance + */ + abstract JsonValue createValue(BigInteger value); + + /** + * Creates a {@link JsonValue} representing the given big decimal using a + * provider-specific mechanism. + * @param value the subject big decimal value + * @return JSON value instance + */ + abstract JsonValue createValue(BigDecimal value); + +} diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java new file mode 100644 index 000000000..f4422a53d --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java @@ -0,0 +1,46 @@ +package org.everit.json.schema.javax.json; + +import org.everit.json.schema.spi.JsonArrayAdapter; + +import javax.json.JsonArray; +import javax.json.JsonValue; +import java.util.Collections; +import java.util.List; + +/** + * A {@link JsonArrayAdapter} that delegates to a JSON-P {@link JsonArray}. + */ +class JavaxJsonArrayAdapter implements JsonArrayAdapter { + + private final JsonArray delegate; + + JavaxJsonArrayAdapter(JsonArray delegate) { + this.delegate = delegate; + } + + @Override + public int length() { + return delegate.size(); + } + + @Override + public JsonValue get(int index) { + return delegate.get(index); + } + + @Override + public void put(int index, JsonValue value) { + delegate.set(index, value); + } + + @Override + public List toList() { + return Collections.unmodifiableList(delegate); + } + + @Override + public JsonValue unwrap() { + return delegate; + } + +} diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java new file mode 100644 index 000000000..a7474c358 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java @@ -0,0 +1,56 @@ +package org.everit.json.schema.javax.json; + +import org.everit.json.schema.spi.JsonObjectAdapter; + +import javax.json.JsonObject; +import javax.json.JsonValue; +import java.util.Collections; +import java.util.Map; + +/** + * A {@link JsonObjectAdapter} that delegates to a JSON-P {@link JsonObject}. + */ +class JavaxJsonObjectAdapter implements JsonObjectAdapter { + + private final JsonObject delegate; + + JavaxJsonObjectAdapter(JsonObject delegate) { + this.delegate = delegate; + } + + @Override + public int length() { + return delegate.size(); + } + + @Override + public String[] keys() { + return delegate.keySet().toArray(new String[0]); + } + + @Override + public boolean has(String key) { + return delegate.containsKey(key); + } + + @Override + public JsonValue get(String key) { + return delegate.get(key); + } + + @Override + public void put(String key, JsonValue value) { + delegate.put(key, value); + } + + @Override + public Map toMap() { + return Collections.unmodifiableMap(delegate); + } + + @Override + public JsonValue unwrap() { + return delegate; + } + +} diff --git a/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java new file mode 100644 index 000000000..3fafca0aa --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java @@ -0,0 +1,47 @@ +package org.everit.json.schema.javax.json; + +import javax.json.Json; +import javax.json.JsonValue; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * An adaptation for the JSON types of JSR-353 for support of Java EE 7. + *

+ * In JSR-353, none of the {@link Json} methods to create JSON scalar + * values are available. However there are methods to create array + * and object builders. To create a single scalar value, in this + * implementation we use an array builder; adding an intrinsic scalar + * value and then extracting it from the completed array. + *

+ * While this approach has some additional overhead, it at least provides + * a means to support Java EE 7 which includes the JSR-353 specification + * of the standard JSON types. + */ +public class Jsr353Adaptation extends JavaxJsonAdaptation { + + JsonValue createValue(String value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + + JsonValue createValue(int value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + + JsonValue createValue(double value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + + JsonValue createValue(long value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + + JsonValue createValue(BigInteger value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + + JsonValue createValue(BigDecimal value) { + return Json.createArrayBuilder().add(value).build().get(0); + } + +} diff --git a/core/src/main/java/org/everit/json/schema/javax/json/Jsr374Adaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/Jsr374Adaptation.java new file mode 100644 index 000000000..8ca90c2d6 --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/Jsr374Adaptation.java @@ -0,0 +1,43 @@ +package org.everit.json.schema.javax.json; + +import javax.json.Json; +import javax.json.JsonValue; +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * An adaptation for the JSON types of JSR-374 for support of Java EE 8. + */ +public class Jsr374Adaptation extends JavaxJsonAdaptation { + + @Override + JsonValue createValue(String value) { + return Json.createValue(value); + } + + @Override + JsonValue createValue(int value) { + return Json.createValue(value); + } + + @Override + JsonValue createValue(double value) { + return Json.createValue(value); + } + + @Override + JsonValue createValue(long value) { + return Json.createValue(value); + } + + @Override + JsonValue createValue(BigInteger value) { + return Json.createValue(value); + } + + @Override + JsonValue createValue(BigDecimal value) { + return Json.createValue(value); + } + +} diff --git a/core/src/main/java/org/everit/json/schema/javax/json/package-info.java b/core/src/main/java/org/everit/json/schema/javax/json/package-info.java new file mode 100644 index 000000000..8f9b263fb --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/javax/json/package-info.java @@ -0,0 +1,4 @@ +/** + * Adaptation for the JSON-P types of {@code javax.json} + */ +package org.everit.json.schema.javax.json; \ No newline at end of file diff --git a/core/src/test/java/org/everit/json/schema/JavaxJsonValidatingVisitorTest.java b/core/src/test/java/org/everit/json/schema/JavaxJsonValidatingVisitorTest.java new file mode 100644 index 000000000..3d21ae0ad --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/JavaxJsonValidatingVisitorTest.java @@ -0,0 +1,154 @@ +/* + * File created on Sep 12, 2018 + * + * Copyright (c) 2018 Carl Harris, Jr + * and others as noted + * + * 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 org.everit.json.schema; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.everit.json.schema.javax.json.Jsr353Adaptation; +import org.everit.json.schema.javax.json.Jsr374Adaptation; +import org.everit.json.schema.spi.JsonAdaptation; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@RunWith(JUnitParamsRunner.class) +public class JavaxJsonValidatingVisitorTest { + + private static final Jsr374Adaptation JSR_374_ADAPTATION = new Jsr374Adaptation(); + private static final Jsr353Adaptation JSR_353_ADAPTATION = new Jsr353Adaptation(); + + private ValidationFailureReporter reporter; + + @Before + public void before() { + reporter = mock(ValidationFailureReporter.class); + } + + public Object[] javaxJsonAdaptations() { + return new Object[] { JSR_374_ADAPTATION, JSR_353_ADAPTATION }; + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_otherType_noRequires(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), false, null)); + verifyZeroInteractions(reporter); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_otherType_requires(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), true, null)); + verify(reporter).failure(JsonObject.class, "string"); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_otherType_nullPermitted_nullObject(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor(JsonValue.NULL, reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), true, Boolean.TRUE)); + verifyZeroInteractions(reporter); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_otherType_nullPermitted_nullReference(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), true, Boolean.TRUE)); + verifyZeroInteractions(reporter); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_nullPermitted_nonNullValue(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), true, Boolean.TRUE)); + verify(reporter).failure(jsonAdaptation.objectType(), "string"); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_requiresType_nullableIsNull(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor(null, reporter, null, jsonAdaptation); + assertFalse(subject.passesTypeCheck(jsonAdaptation.objectType(), true, null)); + verify(reporter).failure(jsonAdaptation.objectType(), null); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void passesTypeCheck_sameType(JsonAdaptation jsonAdaptation) { + ValidatingVisitor subject = new ValidatingVisitor("string", reporter, null, jsonAdaptation); + assertTrue(subject.passesTypeCheck(String.class, true, Boolean.TRUE)); + verifyZeroInteractions(reporter); + } + + public Object[] permittedTypes() { + return new Object[] { + new Object[] { "str" }, + new Object[] { 1 }, + new Object[] { 1L }, + new Object[] { 1.0 }, + new Object[] { 1.0f }, + new Object[] { new BigInteger("42") }, + new Object[] { new BigDecimal("42.3") }, + new Object[] { true }, + new Object[] { null }, + new Object[] { JsonValue.NULL }, + new Object[] { JsonValue.FALSE }, + new Object[] { JsonValue.TRUE }, + new Object[] { Json.createValue("str") }, + new Object[] { Json.createValue(1) }, + new Object[] { Json.createObjectBuilder().build() }, + new Object[] { Json.createArrayBuilder().build() }, + }; + } + + public Object[] notPermittedTypes() { + return new Object[] { + new Object[] { new ArrayList() }, + new Object[] { new RuntimeException() } + }; + } + + @Test + @Parameters(method = "permittedTypes") + public void permittedTypeSuccess(Object subject) { + new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE, JSR_374_ADAPTATION); + } + + @Test(expected = IllegalArgumentException.class) + @Parameters(method = "notPermittedTypes") + public void notPermittedTypeFailure(Object subject) { + new ValidatingVisitor(subject, reporter, ReadWriteValidator.NONE, JSR_374_ADAPTATION); + } + +} From 5facd58f9aca4fabcc57c8d939bf8ca708725940 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:19:55 -0400 Subject: [PATCH 05/14] add README describing the proposed change --- README-adaptation.md | 172 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 README-adaptation.md diff --git a/README-adaptation.md b/README-adaptation.md new file mode 100644 index 000000000..9dc7a7af9 --- /dev/null +++ b/README-adaptation.md @@ -0,0 +1,172 @@ + +A proposal for supporting the JSON-P types of the `javax.json` package. + + +## Motivation + +The JSON-P types provide the underlying JSON abstraction for applications +that utilize Java EE 7 or Java EE 8. In both EE 7 and 8 these types are +commonly employed in building REST APIs using JAX-RS, where high quality +JSON schema validation support for the Everit library would provide great +value. Java SE applications using the Jersey REST server or client +components would similarly benefit from JSON schema validation support +provided by the Everit libary. + +As JSON schema continues to make progress on the IETF standards track, its +use with the Java platform will need to be standardized through the Java +Community Process. Supporting the JSON-P types in this library will +provide an example implementation of JSON schema support for Java that fits +naturally within the existing support for JSON-P and JSON-B, and will +position the author(s) of the Everit library to significantly influence +the direction of the standardization process for JSON Schema support in +Java. + +## Goals + +1. Provide a means for the Everit JSON Schema library to support + validation of JSON values (structures and scalars) using the JSON-P + types from JSR-374 (and its predecessor JSR-353) in the `javax.json` + package. +2. Ensure full backwards compatibility for use of the library with + `org.json` types. +3. Minimize performance impacts of supporting more than one JSON types + representation. +4. Provide an SPI that would facilitate later adaptation to future + specifications as well as other Java JSON implementations (e.g. Jackson) +5. Allow the use of alternative JSON-P type systems to be configurable + at runtime. + +## Non-Goals + +1. Replacement of the underlying JSON type system used to represent JSON + Schema structures in the library. +2. Implementation of support for type systems other than the existing + `org.json` and the JSON-P types. + +## Analysis + +Using the current releases of the Everit JSON Schema library, one +supported approach for using a JSON type system other than that provided +by `org.json` is _translation_. In an application that uses the JSON-P +types in `javax.json`, it is necessary to transform JSON structures from +the JSON-P types to instances of `org.json.JSONObject` and/or +`org.json.JSONArray`. While the translation itself is reasonably +straightforward, it is a very expensive approach and may not be feasible +in applications that need to support JSON schema validation operations at +a high volume per unit time. + +Another potential approach would be to adapt the JSON-P data types to the +API provided by the `org.json` types. This approach is significantly +complicated by the fact that these types are concrete classes rather than +interfaces. Any such approach would involve creating new concrete +subtypes that override virtually all of the functionality of the base types +without violating the contract of the base types. This is further +exacerbated by the fact that these types expose a very large number of +public methods which would need overrides. It is not clear that these +types were designed to be subclassed in this manner, and there may well be +implicit assumptions that are inherent to the design of these types that +make such an approach infeasible. + +After extensive study of the architecture and implementation of the Everit +library, it becomes clear that there exists an (implicit) contract between +the validation implementation and the JSON types provided by `org.json`; +i.e. that while the `org.json` types provide a large API surface, only a +small subset of this is actually used in the validation implementation. +Moreover the scope of implementation classes that utilize this implicit +contract is relatively small due to the visitor-based relationship +between the JSON schema abstraction and the classes that implement +validation. + +In this contract, the JSON type system is assumed to provide a JSON object +type with a very minimal `Map`-like interface. Similarly, the JSON array +type is assumed to provide a minimal `List`-like interface. Lastly, it is +assumed that the JSON type uses the _wrapper_ form of the intrinsic Java +types for strings, booleans, numbers. With respect to nullity, it assumes +that the JSON type system supports not only the intrinsic Java `null` but +also a _null object_; i.e. an instance of some Java type that represents +the null JSON value. + +With this understanding it becomes clear that by creating a service +provider interface (SPI) that describes the contract between the JSON type +system and the validation components, it would then be possible to substitute +a different JSON type system. The SPI will act as a thin adaptation layer +between the validation components and the JSON type system. At the +expense of some additional method call dispatches through the adapter +layer at runtime, the validation components can easily support a different +JSON type system. + +It is important to note that in this approach, neither the interface nor +the implementation of the schema components of the library are changed. +The `Schema` type and its related components are treated as a black box +and its underlying JSON type is irrelevant. Only the validation components +need to support adaptation to a different JSON type system. + +## Proposal + +The proposal represented in the code of this pull request is as follows. + +1. Introduce a `JsonAdaptation` service provider interface (SPI), that + represents the relatively small contract between validation components + and the JSON type system. +2. Implement the SPI using the `org.json` types and use this adaptation + as the default provider in the validation components. This ensures that + existing uses of the library are unaffected. +3. Introduce an implementation of the SPI using the JSON-P types of + the `javax.json` package and provide the means to configure the + `ValidatorBuilder` to use this as an alternative to the default + adaptation to the `org.json` types. + +As can be observed in reviewing the pull request, the amount of change +required in the validation components is small and highly targeted. By +defining the SPI according to the implicit contract between validation +components and the `org.json` types, very little change is needed in most +of the validation components. + +Moreover, using this approach the only required change to existing unit +tests for the library is to acknowledge the default `org.json` adaptation +in the `ValidatingVisitorTest`; i.e. passing an additional argument to +to `ValidatingVisitor` constructor invocations in the test. With +_no other changes_ to the test suite, **all unit tests pass**. Assuming +that test coverage is reasonably complete, this should give some measure +of confidence that the introduction of the adaptation layer does not +change the externally visible behavior of the validation components. + + +## Performance Considerations + +Adapting the library to support another type system will inevitably involve +some trade-off of performance for flexibility. But the approach taken here +aims to minimize this impact. In the default `org.json` adaptation additional +method call dispatches through the adaptation layer should introduce +reasonably small additional overhead. These adapters are quite simple with +minimal branching logic, and conditional expressions that involve few operands +and corresponding fetches. Performance profiling could be used to validate +these assertions. + +An additional performance improvement could be made and has been marked with +a TODO in the implementation. In the `getFailureOfSchema` method of +`ValidatingVisitor`, the adapted JSON value received as input from the caller +is passed to the adaptation layer to be inverted (un-adapted, so to speak). +This is done because there are a handful of test cases that make assumptions +that are violated by the presence of adapter types. This approach was used to +avoid modifying the tests at the same time the adaptation layer is introduced, +to minimize risks. However, after some successful experience with the +adaptation layer, it would be desirable to revisit the design of these tests +to avoid the additional call back into the adaptation layer in this method. + + + + + + + + + + + + + + + + + From 513a61752b2acd871b812ca142c98f1baf57d941 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 08:33:26 -0400 Subject: [PATCH 06/14] remove leftover from previous debugging that breaks some tests --- core/src/main/java/org/everit/json/schema/ValidatingVisitor.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java b/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java index b022daa86..2e4acb67b 100644 --- a/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java @@ -96,7 +96,6 @@ void visitConstSchema(ConstSchema constSchema) { @Override void visitEnumSchema(EnumSchema enumSchema) { - if (jsonAdaptation.isNull(subject)) return; Object effectiveSubject = toJavaValue(jsonAdaptation.adapt(subject)); for (Object possibleValue : enumSchema.getPossibleValues()) { if (ObjectComparator.deepEquals(possibleValue, effectiveSubject)) { From 19313becc5bb2db26abd0a6206f04e0e054e312d Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 09:34:42 -0400 Subject: [PATCH 07/14] add missing @Override annotations --- .../org/everit/json/schema/javax/json/Jsr353Adaptation.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java index 3fafca0aa..734b5e7fc 100644 --- a/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java +++ b/core/src/main/java/org/everit/json/schema/javax/json/Jsr353Adaptation.java @@ -20,26 +20,32 @@ */ public class Jsr353Adaptation extends JavaxJsonAdaptation { + @Override JsonValue createValue(String value) { return Json.createArrayBuilder().add(value).build().get(0); } + @Override JsonValue createValue(int value) { return Json.createArrayBuilder().add(value).build().get(0); } + @Override JsonValue createValue(double value) { return Json.createArrayBuilder().add(value).build().get(0); } + @Override JsonValue createValue(long value) { return Json.createArrayBuilder().add(value).build().get(0); } + @Override JsonValue createValue(BigInteger value) { return Json.createArrayBuilder().add(value).build().get(0); } + @Override JsonValue createValue(BigDecimal value) { return Json.createArrayBuilder().add(value).build().get(0); } From 6be5a49372168d895e273bec4e2b385a926e77b3 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 09:36:42 -0400 Subject: [PATCH 08/14] some README cleanup --- README-adaptation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README-adaptation.md b/README-adaptation.md index 9dc7a7af9..d2beacc1a 100644 --- a/README-adaptation.md +++ b/README-adaptation.md @@ -7,10 +7,10 @@ A proposal for supporting the JSON-P types of the `javax.json` package. The JSON-P types provide the underlying JSON abstraction for applications that utilize Java EE 7 or Java EE 8. In both EE 7 and 8 these types are commonly employed in building REST APIs using JAX-RS, where high quality -JSON schema validation support for the Everit library would provide great -value. Java SE applications using the Jersey REST server or client -components would similarly benefit from JSON schema validation support -provided by the Everit libary. +JSON schema validation support provided from the Everit library would +provide great value. Java SE applications using the Jersey REST server +or client components would similarly benefit from JSON schema validation +support. As JSON schema continues to make progress on the IETF standards track, its use with the Java platform will need to be standardized through the Java From d9c82a447157e6662e65bb075135d36f2f124250 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 09:37:31 -0400 Subject: [PATCH 09/14] more README cleanup --- README-adaptation.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README-adaptation.md b/README-adaptation.md index d2beacc1a..3c383662d 100644 --- a/README-adaptation.md +++ b/README-adaptation.md @@ -153,20 +153,3 @@ avoid modifying the tests at the same time the adaptation layer is introduced, to minimize risks. However, after some successful experience with the adaptation layer, it would be desirable to revisit the design of these tests to avoid the additional call back into the adaptation layer in this method. - - - - - - - - - - - - - - - - - From 3a632f5661dd2b9fe77c94f3c574af52dd378d64 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 14:13:46 -0400 Subject: [PATCH 10/14] remove the README used to develop the content for the PR --- README-adaptation.md | 155 ------------------------------------------- 1 file changed, 155 deletions(-) delete mode 100644 README-adaptation.md diff --git a/README-adaptation.md b/README-adaptation.md deleted file mode 100644 index 3c383662d..000000000 --- a/README-adaptation.md +++ /dev/null @@ -1,155 +0,0 @@ - -A proposal for supporting the JSON-P types of the `javax.json` package. - - -## Motivation - -The JSON-P types provide the underlying JSON abstraction for applications -that utilize Java EE 7 or Java EE 8. In both EE 7 and 8 these types are -commonly employed in building REST APIs using JAX-RS, where high quality -JSON schema validation support provided from the Everit library would -provide great value. Java SE applications using the Jersey REST server -or client components would similarly benefit from JSON schema validation -support. - -As JSON schema continues to make progress on the IETF standards track, its -use with the Java platform will need to be standardized through the Java -Community Process. Supporting the JSON-P types in this library will -provide an example implementation of JSON schema support for Java that fits -naturally within the existing support for JSON-P and JSON-B, and will -position the author(s) of the Everit library to significantly influence -the direction of the standardization process for JSON Schema support in -Java. - -## Goals - -1. Provide a means for the Everit JSON Schema library to support - validation of JSON values (structures and scalars) using the JSON-P - types from JSR-374 (and its predecessor JSR-353) in the `javax.json` - package. -2. Ensure full backwards compatibility for use of the library with - `org.json` types. -3. Minimize performance impacts of supporting more than one JSON types - representation. -4. Provide an SPI that would facilitate later adaptation to future - specifications as well as other Java JSON implementations (e.g. Jackson) -5. Allow the use of alternative JSON-P type systems to be configurable - at runtime. - -## Non-Goals - -1. Replacement of the underlying JSON type system used to represent JSON - Schema structures in the library. -2. Implementation of support for type systems other than the existing - `org.json` and the JSON-P types. - -## Analysis - -Using the current releases of the Everit JSON Schema library, one -supported approach for using a JSON type system other than that provided -by `org.json` is _translation_. In an application that uses the JSON-P -types in `javax.json`, it is necessary to transform JSON structures from -the JSON-P types to instances of `org.json.JSONObject` and/or -`org.json.JSONArray`. While the translation itself is reasonably -straightforward, it is a very expensive approach and may not be feasible -in applications that need to support JSON schema validation operations at -a high volume per unit time. - -Another potential approach would be to adapt the JSON-P data types to the -API provided by the `org.json` types. This approach is significantly -complicated by the fact that these types are concrete classes rather than -interfaces. Any such approach would involve creating new concrete -subtypes that override virtually all of the functionality of the base types -without violating the contract of the base types. This is further -exacerbated by the fact that these types expose a very large number of -public methods which would need overrides. It is not clear that these -types were designed to be subclassed in this manner, and there may well be -implicit assumptions that are inherent to the design of these types that -make such an approach infeasible. - -After extensive study of the architecture and implementation of the Everit -library, it becomes clear that there exists an (implicit) contract between -the validation implementation and the JSON types provided by `org.json`; -i.e. that while the `org.json` types provide a large API surface, only a -small subset of this is actually used in the validation implementation. -Moreover the scope of implementation classes that utilize this implicit -contract is relatively small due to the visitor-based relationship -between the JSON schema abstraction and the classes that implement -validation. - -In this contract, the JSON type system is assumed to provide a JSON object -type with a very minimal `Map`-like interface. Similarly, the JSON array -type is assumed to provide a minimal `List`-like interface. Lastly, it is -assumed that the JSON type uses the _wrapper_ form of the intrinsic Java -types for strings, booleans, numbers. With respect to nullity, it assumes -that the JSON type system supports not only the intrinsic Java `null` but -also a _null object_; i.e. an instance of some Java type that represents -the null JSON value. - -With this understanding it becomes clear that by creating a service -provider interface (SPI) that describes the contract between the JSON type -system and the validation components, it would then be possible to substitute -a different JSON type system. The SPI will act as a thin adaptation layer -between the validation components and the JSON type system. At the -expense of some additional method call dispatches through the adapter -layer at runtime, the validation components can easily support a different -JSON type system. - -It is important to note that in this approach, neither the interface nor -the implementation of the schema components of the library are changed. -The `Schema` type and its related components are treated as a black box -and its underlying JSON type is irrelevant. Only the validation components -need to support adaptation to a different JSON type system. - -## Proposal - -The proposal represented in the code of this pull request is as follows. - -1. Introduce a `JsonAdaptation` service provider interface (SPI), that - represents the relatively small contract between validation components - and the JSON type system. -2. Implement the SPI using the `org.json` types and use this adaptation - as the default provider in the validation components. This ensures that - existing uses of the library are unaffected. -3. Introduce an implementation of the SPI using the JSON-P types of - the `javax.json` package and provide the means to configure the - `ValidatorBuilder` to use this as an alternative to the default - adaptation to the `org.json` types. - -As can be observed in reviewing the pull request, the amount of change -required in the validation components is small and highly targeted. By -defining the SPI according to the implicit contract between validation -components and the `org.json` types, very little change is needed in most -of the validation components. - -Moreover, using this approach the only required change to existing unit -tests for the library is to acknowledge the default `org.json` adaptation -in the `ValidatingVisitorTest`; i.e. passing an additional argument to -to `ValidatingVisitor` constructor invocations in the test. With -_no other changes_ to the test suite, **all unit tests pass**. Assuming -that test coverage is reasonably complete, this should give some measure -of confidence that the introduction of the adaptation layer does not -change the externally visible behavior of the validation components. - - -## Performance Considerations - -Adapting the library to support another type system will inevitably involve -some trade-off of performance for flexibility. But the approach taken here -aims to minimize this impact. In the default `org.json` adaptation additional -method call dispatches through the adaptation layer should introduce -reasonably small additional overhead. These adapters are quite simple with -minimal branching logic, and conditional expressions that involve few operands -and corresponding fetches. Performance profiling could be used to validate -these assertions. - -An additional performance improvement could be made and has been marked with -a TODO in the implementation. In the `getFailureOfSchema` method of -`ValidatingVisitor`, the adapted JSON value received as input from the caller -is passed to the adaptation layer to be inverted (un-adapted, so to speak). -This is done because there are a handful of test cases that make assumptions -that are violated by the presence of adapter types. This approach was used to -avoid modifying the tests at the same time the adaptation layer is introduced, -to minimize risks. However, after some successful experience with the -adaptation layer, it would be desirable to revisit the design of these tests -to avoid the additional call back into the adaptation layer in this method. From 4a894c24e20976bbc194831c8cf44277082bbd85 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Thu, 13 Sep 2018 16:36:08 -0400 Subject: [PATCH 11/14] added some tests for the org.json adaptation --- .../json/schema/JSONAdaptationTest.java | 98 +++++++++++++++++++ .../json/schema/JSONArrayAdapterTest.java | 23 +++++ .../json/schema/JSONObjectAdapterTest.java | 47 +++++++++ 3 files changed, 168 insertions(+) create mode 100644 core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java create mode 100644 core/src/test/java/org/everit/json/schema/JSONArrayAdapterTest.java create mode 100644 core/src/test/java/org/everit/json/schema/JSONObjectAdapterTest.java diff --git a/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java b/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java new file mode 100644 index 000000000..c48fea727 --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java @@ -0,0 +1,98 @@ +package org.everit.json.schema; + +import org.everit.json.schema.spi.JsonAdapter; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +public class JSONAdaptationTest { + + private final JSONAdaptation adaptation = new JSONAdaptation(); + + @Test + public void testArrayType() { + assertEquals(JSONArray.class, adaptation.arrayType()); + } + + @Test + public void testObjectType() { + assertEquals(JSONObject.class, adaptation.objectType()); + } + + @Test + public void testIsSupportedType() { + assertTrue(adaptation.isSupportedType(JSONArray.class)); + assertTrue(adaptation.isSupportedType(JSONObject.class)); + assertTrue(adaptation.isSupportedType(JSONObject.NULL.getClass())); + } + + @Test + public void testSupportedTypes() { + final List> types = Arrays.asList(adaptation.supportedTypes()); + assertEquals(3, types.size()); + assertTrue(types.contains(JSONArray.class)); + assertTrue(types.contains(JSONObject.class)); + assertTrue(types.contains(JSONObject.NULL.getClass())); + } + + @Test + public void testAdaptIntrinsics() { + assertEquals("value", adaptation.adapt("value")); + assertEquals(true, adaptation.adapt(true)); + assertEquals(1, adaptation.adapt(1)); + assertNull(adaptation.adapt(null)); + } + + @Test + public void testAdaptAdapter() { + final JsonAdapter adapter = () -> null; + assertSame(adapter, adaptation.adapt(adapter)); + } + + @Test + public void testAdaptJSONNull() { + assertNull(adaptation.adapt(JSONObject.NULL)); + } + + @Test + public void testAdaptJSONObject() { + final JSONObject object = new JSONObject(); + final Object result = adaptation.adapt(object); + assertTrue(result instanceof JSONObjectAdapter); + assertSame(object, ((JSONObjectAdapter) result).unwrap()); + } + + @Test + public void testAdaptJSONArray() { + final JSONArray array = new JSONArray(); + final Object result = adaptation.adapt(array); + assertTrue(result instanceof JSONArrayAdapter); + assertSame(array, ((JSONArrayAdapter) result).unwrap()); + } + + @Test + public void testInvertIntrinsics() { + assertEquals("value", adaptation.invert("value")); + assertEquals(true, adaptation.invert(true)); + assertEquals(1, adaptation.invert(1)); + assertEquals(JSONObject.NULL, adaptation.adapt(null)); + } + + @Test + public void testInvertObjectAdapter() { + final JSONObject object = new JSONObject(); + assertSame(object, adaptation.invert(new JSONObjectAdapter(object))); + } + + @Test + public void testInvertArrayAdapter() { + final JSONArray array = new JSONArray(); + assertSame(array, adaptation.invert(new JSONArrayAdapter(array))); + } + +} diff --git a/core/src/test/java/org/everit/json/schema/JSONArrayAdapterTest.java b/core/src/test/java/org/everit/json/schema/JSONArrayAdapterTest.java new file mode 100644 index 000000000..eb82907fb --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/JSONArrayAdapterTest.java @@ -0,0 +1,23 @@ +package org.everit.json.schema; + +import org.json.JSONArray; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class JSONArrayAdapterTest { + + @Test + public void testAdapter() { + final JSONArrayAdapter adapter = new JSONArrayAdapter( + new JSONArray().put("value")); + + assertEquals(1, adapter.length()); + assertEquals("value", adapter.get(0)); + assertEquals("value", adapter.toList().get(0)); + + adapter.put(0, "otherValue"); + assertEquals("otherValue", adapter.get(0)); + } + +} diff --git a/core/src/test/java/org/everit/json/schema/JSONObjectAdapterTest.java b/core/src/test/java/org/everit/json/schema/JSONObjectAdapterTest.java new file mode 100644 index 000000000..3e2f4efdd --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/JSONObjectAdapterTest.java @@ -0,0 +1,47 @@ +/* + * File created on Sep 13, 2018 + * + * Copyright (c) 2018 Carl Harris, Jr + * and others as noted + * + * 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 org.everit.json.schema; + +import org.json.JSONObject; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JSONObjectAdapterTest { + + @Test + public void testAdapter() { + final JSONObjectAdapter adapter = new JSONObjectAdapter( + new JSONObject().put("key", "value")); + + assertEquals(1, adapter.length()); + assertTrue(adapter.has("key")); + assertEquals("value", adapter.get("key")); + assertTrue(Arrays.asList(adapter.keys()).contains("key")); + assertTrue(adapter.toMap().containsKey("key")); + assertEquals("value", adapter.toMap().get("key")); + + adapter.put("key", "otherValue"); + assertEquals("otherValue", adapter.get("key")); + } + +} From 9deea994785065b3b9e4b6a1078ebfcbf1e61ccd Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Fri, 14 Sep 2018 05:33:27 -0400 Subject: [PATCH 12/14] added tests for javax.json adaptation --- .../javax/json/JavaxJsonAdaptation.java | 10 +- .../javax/json/JavaxJsonArrayAdapter.java | 2 +- .../javax/json/JavaxJsonObjectAdapter.java | 2 +- .../json/schema/JSONAdaptationTest.java | 2 +- .../javax/json/JavaxJsonArrayAdapterTest.java | 45 ++++++ .../json/JavaxJsonObjectAdapterTest.java | 50 +++++++ .../javax/json/JavaxJsonValidationTest.java | 133 ++++++++++++++++++ 7 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapterTest.java create mode 100644 core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapterTest.java create mode 100644 core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java index 6cf4dd6f5..c927dbb1f 100644 --- a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java @@ -124,14 +124,20 @@ public boolean isNull(Object value) { @Override public Object adapt(Object value) { - if (value == JsonValue.TRUE) { + if (value == JsonValue.NULL) { + return null; + } else if (value == JsonValue.TRUE) { return true; } else if (value == JsonValue.FALSE) { return false; } else if (value instanceof JsonString) { return ((JsonString) value).getString(); } else if (value instanceof JsonNumber) { - return ((JsonNumber) value).bigDecimalValue(); + if (((JsonNumber) value).isIntegral()) { + return ((JsonNumber) value).bigIntegerValue(); + } else { + return ((JsonNumber) value).bigDecimalValue(); + } } else if (value instanceof JsonArray) { return new JavaxJsonArrayAdapter((JsonArray) value); } else if (value instanceof JsonObject) { diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java index f4422a53d..31f9851d6 100644 --- a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapter.java @@ -30,7 +30,7 @@ public JsonValue get(int index) { @Override public void put(int index, JsonValue value) { - delegate.set(index, value); + throw new UnsupportedOperationException(); } @Override diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java index a7474c358..75a79b6f0 100644 --- a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapter.java @@ -40,7 +40,7 @@ public JsonValue get(String key) { @Override public void put(String key, JsonValue value) { - delegate.put(key, value); + throw new UnsupportedOperationException(); } @Override diff --git a/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java b/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java index c48fea727..0b22668db 100644 --- a/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java +++ b/core/src/test/java/org/everit/json/schema/JSONAdaptationTest.java @@ -80,7 +80,7 @@ public void testInvertIntrinsics() { assertEquals("value", adaptation.invert("value")); assertEquals(true, adaptation.invert(true)); assertEquals(1, adaptation.invert(1)); - assertEquals(JSONObject.NULL, adaptation.adapt(null)); + assertEquals(JSONObject.NULL, adaptation.invert(null)); } @Test diff --git a/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapterTest.java b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapterTest.java new file mode 100644 index 000000000..ebae31c74 --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonArrayAdapterTest.java @@ -0,0 +1,45 @@ +/* + * File created on Sep 14, 2018 + * + * Copyright (c) 2018 Carl Harris, Jr + * and others as noted + * + * 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 org.everit.json.schema.javax.json; + +import org.junit.Test; + +import javax.json.Json; + +import static org.junit.Assert.assertEquals; + +public class JavaxJsonArrayAdapterTest { + + private final JavaxJsonArrayAdapter adapter = new JavaxJsonArrayAdapter( + Json.createArrayBuilder().add("value").build()); + + @Test + public void testAdapter() { + + assertEquals(1, adapter.length()); + assertEquals(Json.createValue("value"), adapter.get(0)); + assertEquals(Json.createValue("value"), adapter.toList().get(0)); + } + + @Test(expected = UnsupportedOperationException.class) + public void testAdapterPut() { + adapter.put(0, Json.createValue("value")); + } + +} diff --git a/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapterTest.java b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapterTest.java new file mode 100644 index 000000000..1969891ad --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonObjectAdapterTest.java @@ -0,0 +1,50 @@ +/* + * File created on Sep 14, 2018 + * + * Copyright (c) 2018 Carl Harris, Jr + * and others as noted + * + * 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 org.everit.json.schema.javax.json; + +import org.junit.Test; + +import javax.json.Json; + +import java.util.Arrays; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JavaxJsonObjectAdapterTest { + + private final JavaxJsonObjectAdapter adapter = new JavaxJsonObjectAdapter( + Json.createObjectBuilder().add("key", "value").build()); + + @Test + public void testAdapter() { + assertEquals(1, adapter.length()); + assertTrue(adapter.has("key")); + assertEquals(Json.createValue("value"), adapter.get("key")); + assertTrue(Arrays.asList(adapter.keys()).contains("key")); + assertEquals(Json.createValue("value"), adapter.toMap().get("key")); + } + + @Test(expected = UnsupportedOperationException.class) + public void testAdapterPut() { + adapter.put("key", Json.createValue("value")); + } + +} diff --git a/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java new file mode 100644 index 000000000..5199eb12d --- /dev/null +++ b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java @@ -0,0 +1,133 @@ +package org.everit.json.schema.javax.json; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.everit.json.schema.spi.JsonAdapter; +import org.everit.json.schema.spi.JsonArrayAdapter; +import org.everit.json.schema.spi.JsonObjectAdapter; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.json.*; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +@RunWith(JUnitParamsRunner.class) +public class JavaxJsonValidationTest { + + private static final Jsr374Adaptation JSR_374_ADAPTATION = new Jsr374Adaptation(); + private static final Jsr353Adaptation JSR_353_ADAPTATION = new Jsr353Adaptation(); + + public Object[] javaxJsonAdaptations() { + return new Object[] { JSR_374_ADAPTATION, JSR_353_ADAPTATION }; + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testArrayType(JavaxJsonAdaptation adaptation) { + assertEquals(JsonArray.class, adaptation.arrayType()); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testObjectType(JavaxJsonAdaptation adaptation) { + assertEquals(JsonObject.class, adaptation.objectType()); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testIsSupportedType(JavaxJsonAdaptation adaptation) { + assertTrue(adaptation.isSupportedType(JsonValue.class)); + assertTrue(adaptation.isSupportedType(JsonNumber.class)); + assertTrue(adaptation.isSupportedType(JsonString.class)); + assertTrue(adaptation.isSupportedType(JsonObject.class)); + assertTrue(adaptation.isSupportedType(JsonArray.class)); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testSupportedTypes(JavaxJsonAdaptation adaptation) { + final List> types = Arrays.asList(adaptation.supportedTypes()); + assertEquals(1, types.size()); + assertTrue(types.contains(JsonValue.class)); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testAdaptIntrinsics(JavaxJsonAdaptation adaptation) { + assertEquals("value", adaptation.adapt("value")); + assertEquals(true, adaptation.adapt(true)); + assertEquals(1, adaptation.adapt(1)); + assertNull(adaptation.adapt(null)); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testAdaptAdapter(JavaxJsonAdaptation adaptation) { + final JsonAdapter adapter = () -> null; + assertSame(adapter, adaptation.adapt(adapter)); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testAdaptJsonScalars(JavaxJsonAdaptation adaptation) { + assertNull(adaptation.adapt(JsonValue.NULL)); + assertEquals(true, adaptation.adapt(JsonValue.TRUE)); + assertEquals(false, adaptation.adapt(JsonValue.FALSE)); + assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(1))); + assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(1L))); + assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(BigInteger.ONE))); + assertEquals(BigDecimal.valueOf(1.1), adaptation.adapt(Json.createValue(BigDecimal.valueOf(1.1)))); + assertEquals(BigDecimal.valueOf(1.1), adaptation.adapt(Json.createValue(1.1))); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testAdaptJsonObject(JavaxJsonAdaptation adaptation) { + final JsonObject object = Json.createObjectBuilder().build(); + final Object result = adaptation.adapt(object); + assertTrue(result instanceof JsonObjectAdapter); + assertSame(object, ((JsonObjectAdapter) result).unwrap()); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testAdaptJsonArray(JavaxJsonAdaptation adaptation) { + final JsonArray object = Json.createArrayBuilder().build(); + final Object result = adaptation.adapt(object); + assertTrue(result instanceof JsonArrayAdapter); + assertSame(object, ((JsonArrayAdapter) result).unwrap()); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testInvertIntrinsics(JavaxJsonAdaptation adaptation) { + assertEquals(JsonValue.NULL, adaptation.invert(null)); + assertEquals(JsonValue.TRUE, adaptation.invert(true)); + assertEquals(JsonValue.FALSE, adaptation.invert(false)); + assertEquals(Json.createValue("value"), adaptation.invert("value")); + assertEquals(Json.createValue(1), adaptation.invert(1)); + assertEquals(Json.createValue(1L), adaptation.invert(1L)); + assertEquals(Json.createValue(BigInteger.ONE), adaptation.invert(BigInteger.ONE)); + assertEquals(Json.createValue(BigDecimal.ONE), adaptation.invert(BigDecimal.ONE)); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testInvertObjectAdapter(JavaxJsonAdaptation adaptation) { + final JsonObject object = Json.createObjectBuilder().build(); + assertSame(object, adaptation.invert(new JavaxJsonObjectAdapter(object))); + } + + @Test + @Parameters(method = "javaxJsonAdaptations") + public void testInvertArrayAdapter(JavaxJsonAdaptation adaptation) { + final JsonArray array = Json.createArrayBuilder().build(); + assertSame(array, adaptation.invert(new JavaxJsonArrayAdapter(array))); + } + +} From e33b332ed0aa5e410ebbf3a356357a30a9063327 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Fri, 14 Sep 2018 06:09:52 -0400 Subject: [PATCH 13/14] add some failure cases in ObjectComparator tests for adapters --- .../json/schema/ObjectComparatorTest.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/everit/json/schema/ObjectComparatorTest.java b/core/src/test/java/org/everit/json/schema/ObjectComparatorTest.java index 8784152e8..513c3a5b3 100644 --- a/core/src/test/java/org/everit/json/schema/ObjectComparatorTest.java +++ b/core/src/test/java/org/everit/json/schema/ObjectComparatorTest.java @@ -2,6 +2,8 @@ import static org.junit.Assert.assertFalse; +import org.everit.json.schema.spi.JsonArrayAdapter; +import org.everit.json.schema.spi.JsonObjectAdapter; import org.json.JSONArray; import org.json.JSONObject; import org.junit.Test; @@ -17,6 +19,12 @@ public class ObjectComparatorTest { public static final JSONArray EMPTY_ARRAY = new JSONArray(); public static final JSONObject EMPTY_OBJECT = new JSONObject(); + private static final JsonArrayAdapter EMPTY_ARRAY_ADAPTER = + new JSONArrayAdapter(new JSONArray()); + + private static final JsonObjectAdapter EMPTY_OBJECT_ADAPTER = + new JSONObjectAdapter(new JSONObject()); + private Object[][] failingCases() { return new Object[][] { { "array, null", EMPTY_ARRAY, null }, @@ -24,7 +32,17 @@ private Object[][] failingCases() { { "object, null", EMPTY_OBJECT, null }, { "arrays with different length", EMPTY_ARRAY, new JSONArray("[null]") }, { "arrays with different elems", new JSONArray("[true, false]"), new JSONArray("[false, true]") }, - { "objects with different length", EMPTY_OBJECT, new JSONObject("{\"a\":true}") } + { "objects with different length", EMPTY_OBJECT, new JSONObject("{\"a\":true}") }, + { "array adapter, null", EMPTY_ARRAY_ADAPTER, null }, + { "array adapter, object adapter", EMPTY_ARRAY_ADAPTER, EMPTY_OBJECT_ADAPTER }, + { "object adapter, null", EMPTY_OBJECT_ADAPTER, null }, + { "array adapters with different length", EMPTY_ARRAY_ADAPTER, new JSONArrayAdapter(new JSONArray("[null]")) }, + { "array adapters with different elems", new JSONArrayAdapter(new JSONArray("[true, false]")), new JSONArrayAdapter(new JSONArray("[false, true]")) }, + { "object adapters with different length", EMPTY_OBJECT_ADAPTER, new JSONObjectAdapter(new JSONObject("{\"a\":true}")) }, + { "array, array adapter", EMPTY_ARRAY, EMPTY_ARRAY_ADAPTER }, + { "object, object adapter", EMPTY_OBJECT, EMPTY_OBJECT_ADAPTER }, + { "array, object adapter", EMPTY_ARRAY, EMPTY_OBJECT_ADAPTER }, + { "object, array adapter", EMPTY_OBJECT, EMPTY_ARRAY_ADAPTER }, }; } From 5dfcb2bd1d1b053045e4556af8a5dd7752381c51 Mon Sep 17 00:00:00 2001 From: Carl Harris Date: Fri, 14 Sep 2018 06:10:15 -0400 Subject: [PATCH 14/14] add tests for newInstance methods on JavaxJsonAdaptation --- .../javax/json/JavaxJsonAdaptation.java | 31 +++++++++++----- .../javax/json/JavaxJsonValidationTest.java | 37 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java index c927dbb1f..9065f32fe 100644 --- a/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java +++ b/core/src/main/java/org/everit/json/schema/javax/json/JavaxJsonAdaptation.java @@ -24,8 +24,8 @@ */ public abstract class JavaxJsonAdaptation implements JsonAdaptation { - private static final String JSR_374_ADAPTATION = "Jsr374Adaptation"; - private static final String JSR_353_ADAPTATION = "Jsr353Adaptation"; + static final String JSR_374_ADAPTATION = "Jsr374Adaptation"; + static final String JSR_353_ADAPTATION = "Jsr353Adaptation"; @Override public Class arrayType() { @@ -67,17 +67,12 @@ public static JavaxJsonAdaptation newInstance() { * provided by OSGi. * * @param classLoader the class loader to use to find the JSON-P API and - * implementation classes + * implementation classes * @return adaptation instance for JSON-P data types * @throws RuntimeException if the adaptation class cannot be instantiated */ public static JavaxJsonAdaptation newInstance(ClassLoader classLoader) { - try { - Json.class.getMethod("createValue", String.class); - return newInstance(classLoader, JSR_374_ADAPTATION); - } catch (NoSuchMethodException ex) { - return newInstance(classLoader, JSR_353_ADAPTATION); - } + return newInstance(classLoader, determineProvider("createValue", String.class)); } /** @@ -88,7 +83,7 @@ public static JavaxJsonAdaptation newInstance(ClassLoader classLoader) { * @return adaptation instance * @throws RuntimeException an instance of the specified type cannot be instantiated */ - private static JavaxJsonAdaptation newInstance(ClassLoader classLoader, + static JavaxJsonAdaptation newInstance(ClassLoader classLoader, String providerName) { try { return (JavaxJsonAdaptation) classLoader.loadClass( @@ -99,6 +94,22 @@ private static JavaxJsonAdaptation newInstance(ClassLoader classLoader, } } + /** + * Determine the name of the adaptation provider class based on the availability + * of a particular sentinel method in the {@link Json} class. + * @param sentinelMethodName name of the method whose presence is to be checked + * @param argTypes argument types for the sentinel method + * @return adaptation provider name + */ + static String determineProvider(String sentinelMethodName, Class... argTypes) { + try { + Json.class.getMethod(sentinelMethodName, argTypes); + return JSR_374_ADAPTATION; + } catch (NoSuchMethodException ex) { + return JSR_353_ADAPTATION; + } + } + /** * Constructs the fully-qualified class name for a given named provider class. *

diff --git a/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java index 5199eb12d..4758a28ea 100644 --- a/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java +++ b/core/src/test/java/org/everit/json/schema/javax/json/JavaxJsonValidationTest.java @@ -78,6 +78,7 @@ public void testAdaptJsonScalars(JavaxJsonAdaptation adaptation) { assertNull(adaptation.adapt(JsonValue.NULL)); assertEquals(true, adaptation.adapt(JsonValue.TRUE)); assertEquals(false, adaptation.adapt(JsonValue.FALSE)); + assertEquals("value", adaptation.adapt(Json.createValue("value"))); assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(1))); assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(1L))); assertEquals(BigInteger.ONE, adaptation.adapt(Json.createValue(BigInteger.ONE))); @@ -112,6 +113,7 @@ public void testInvertIntrinsics(JavaxJsonAdaptation adaptation) { assertEquals(Json.createValue("value"), adaptation.invert("value")); assertEquals(Json.createValue(1), adaptation.invert(1)); assertEquals(Json.createValue(1L), adaptation.invert(1L)); + assertEquals(Json.createValue(1.0), adaptation.invert(1.0)); assertEquals(Json.createValue(BigInteger.ONE), adaptation.invert(BigInteger.ONE)); assertEquals(Json.createValue(BigDecimal.ONE), adaptation.invert(BigDecimal.ONE)); } @@ -130,4 +132,39 @@ public void testInvertArrayAdapter(JavaxJsonAdaptation adaptation) { assertSame(array, adaptation.invert(new JavaxJsonArrayAdapter(array))); } + @Test + public void testNewInstanceDefaultClassLoader() { + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(null); + assertTrue(JavaxJsonAdaptation.newInstance() instanceof Jsr374Adaptation); + Thread.currentThread().setContextClassLoader(tccl); + } + + @Test + public void testNewInstanceThreadContextClassLoader() { + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + assertTrue(JavaxJsonAdaptation.newInstance() instanceof Jsr374Adaptation); + Thread.currentThread().setContextClassLoader(tccl); + } + + @Test + public void testNewInstanceWithClassLoader() { + assertTrue(JavaxJsonAdaptation.newInstance(getClass().getClassLoader()) + instanceof Jsr374Adaptation); + } + + @Test + public void testDetermineProvider() { + assertEquals(JavaxJsonAdaptation.JSR_374_ADAPTATION, + JavaxJsonAdaptation.determineProvider("createValue", String.class)); + assertEquals(JavaxJsonAdaptation.JSR_353_ADAPTATION, + JavaxJsonAdaptation.determineProvider(".noSuchMethod")); + } + + @Test(expected = RuntimeException.class) + public void testInstanceWhenCannotInstantiate() { + JavaxJsonAdaptation.newInstance(getClass().getClassLoader(), ".noSuchProviderName"); + } + }