From cfca00736718a1c4208f3d0a00f2e542c93fe93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Mar=C3=ADa=20Fern=C3=A1ndez?= Date: Tue, 30 Apr 2019 11:31:31 +0200 Subject: [PATCH 1/5] Unfinished work on customtypes support --- .../json/schema/AbstractCustomTypeSchema.java | 22 ++++ .../everit/json/schema/ValidatingVisitor.java | 5 + .../java/org/everit/json/schema/Visitor.java | 3 + .../json/schema/loader/LoaderConfig.java | 10 +- .../json/schema/loader/SchemaExtractor.java | 27 ++++- .../json/schema/loader/SchemaLoader.java | 52 ++++++++- .../everit/json/schema/CustomTestSchema.java | 108 ++++++++++++++++++ .../CustomTypeSchemaValidatingVisitor.java | 39 +++++++ .../everit/json/schema/CustomTypeTest.java | 54 +++++++++ .../everit/json/schema/customType/fails.json | 3 + .../everit/json/schema/customType/schema.json | 10 ++ .../everit/json/schema/customType/works.json | 3 + 12 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java create mode 100644 tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java create mode 100644 tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java create mode 100644 tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java create mode 100644 tests/vanilla/src/main/resources/org/everit/json/schema/customType/fails.json create mode 100644 tests/vanilla/src/main/resources/org/everit/json/schema/customType/schema.json create mode 100644 tests/vanilla/src/main/resources/org/everit/json/schema/customType/works.json diff --git a/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java new file mode 100644 index 000000000..4fe442f8b --- /dev/null +++ b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java @@ -0,0 +1,22 @@ +package org.everit.json.schema; + +/** + * Superclass of all custom types + */ +public abstract class AbstractCustomTypeSchema extends Schema { + /** + * Constructor. + * + * @param builder + * the builder containing the optional title, description and id attributes of the schema + */ + protected AbstractCustomTypeSchema(Schema.Builder builder) { + super(builder); + } + + /** + * On custom types, it should return an instance of its own visitor implementation + */ + public abstract Visitor buildVisitor(Object subject,ValidatingVisitor owner); + +} \ No newline at end of file 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 5b7b419df..1058ac311 100644 --- a/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java +++ b/core/src/main/java/org/everit/json/schema/ValidatingVisitor.java @@ -148,6 +148,11 @@ void visitStringSchema(StringSchema stringSchema) { stringSchema.accept(new StringSchemaValidatingVisitor(subject, this)); } + @Override + void visitCustomTypeSchema(AbstractCustomTypeSchema customTypeSchema) { + customTypeSchema.accept(customTypeSchema.buildVisitor(subject, this)); + } + @Override void visitCombinedSchema(CombinedSchema combinedSchema) { Collection subschemas = combinedSchema.getSubschemas(); diff --git a/core/src/main/java/org/everit/json/schema/Visitor.java b/core/src/main/java/org/everit/json/schema/Visitor.java index 18d5d12a9..7aee6d2ae 100644 --- a/core/src/main/java/org/everit/json/schema/Visitor.java +++ b/core/src/main/java/org/everit/json/schema/Visitor.java @@ -170,6 +170,9 @@ void visitStringSchema(StringSchema stringSchema) { visitFormat(stringSchema.getFormatValidator()); } + void visitCustomTypeSchema(AbstractCustomTypeSchema customTypeSchema) { + } + void visitFormat(FormatValidator formatValidator) { } diff --git a/core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java b/core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java index dba904211..0120a241a 100644 --- a/core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java +++ b/core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java @@ -6,6 +6,8 @@ import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_6; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; +import java.lang.reflect.Method; + import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -27,6 +29,8 @@ static LoaderConfig defaultV4Config() { final SchemaClient schemaClient; final Map formatValidators; + + final Map customTypes; final Map schemasByURI; @@ -40,15 +44,17 @@ static LoaderConfig defaultV4Config() { LoaderConfig(SchemaClient schemaClient, Map formatValidators, SpecificationVersion specVersion, boolean useDefaults) { - this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory()); + this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory(), emptyMap()); } LoaderConfig(SchemaClient schemaClient, Map formatValidators, Map schemasByURI, SpecificationVersion specVersion, boolean useDefaults, boolean nullableSupport, - RegexpFactory regexpFactory) { + RegexpFactory regexpFactory, + Map customTypes) { this.schemaClient = requireNonNull(schemaClient, "schemaClient cannot be null"); this.formatValidators = requireNonNull(formatValidators, "formatValidators cannot be null"); + this.customTypes = requireNonNull(customTypes, "customTypes cannot be null"); if (schemasByURI == null) { this.schemasByURI = new HashMap<>(); } else { diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java index 8bd604163..8c544de29 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java @@ -9,13 +9,18 @@ import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_4; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import org.everit.json.schema.AbstractCustomTypeSchema; import org.everit.json.schema.ArraySchema; import org.everit.json.schema.BooleanSchema; import org.everit.json.schema.CombinedSchema; @@ -235,9 +240,11 @@ private ConditionalSchema.Builder buildConditionalSchema() { } class TypeBasedSchemaExtractor extends AbstractSchemaExtractor { + private Map customTypes; - TypeBasedSchemaExtractor(SchemaLoader defaultLoader) { + TypeBasedSchemaExtractor(SchemaLoader defaultLoader,Map customTypes) { super(defaultLoader); + this.customTypes = customTypes; } @Override List> extract() { @@ -276,7 +283,23 @@ private Schema.Builder loadForExplicitType(String typeString) { case "object": return buildObjectSchema(); default: - throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("unknown type: [%s]", typeString)); + if(customTypes.isEmpty()) { + System.err.println("UNACCEPTABLE!!!!\n"); + } + if(customTypes.containsKey(typeString)) { + // Calling the public static builder method using the + // Java reflection mechanisms + Method builderMethod = customTypes.get(typeString); + try { + return (Schema.Builder) builderMethod.invoke(null); + } catch(InvocationTargetException ite) { + throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("type: [%s] builder creation has failed", typeString)); + } catch(IllegalAccessException iae) { + throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("type: [%s] builder creation is not allowed", typeString)); + } + } else { + throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("unknown type: [%s]", typeString)); + } } } diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java index 505586514..58f60261c 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java @@ -9,6 +9,9 @@ import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_6; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; @@ -18,6 +21,7 @@ import java.util.Objects; import java.util.Optional; +import org.everit.json.schema.AbstractCustomTypeSchema; import org.everit.json.schema.CombinedSchema; import org.everit.json.schema.EmptySchema; import org.everit.json.schema.FalseSchema; @@ -69,13 +73,51 @@ public static class SchemaLoaderBuilder { private boolean nullableSupport = false; RegexpFactory regexpFactory = new JavaUtilRegexpFactory(); + + Map customTypes = new HashMap<>(); Map schemasByURI = null; public SchemaLoaderBuilder() { setSpecVersion(DRAFT_4); } - + + /** + * Registers a custom schema type + * + * @param typeName + * the type name to use for this custom JSON Schema type + * @param clazz + * the class which implements the validation of this custom JSON Schema type + * @return {@code this} + */ + public SchemaLoaderBuilder addCustomType(String typeName,Class clazz) { + typeName = requireNonNull(typeName, "the name of the custom type cannot be null"); + if(typeName.length() == 0) { + throw new IllegalArgumentException("the name of the custom type must be non-empty"); + } + try { + Method method = clazz.getMethod("builder"); + int mods = method.getModifiers(); + if(!Modifier.isStatic(mods) || !Modifier.isPublic(mods)) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a public static 'builder()' method"); + } + Class retClazz = method.getReturnType(); + retClazz.asSubclass(Schema.Builder.class); + + // If all is ok + customTypes.put(typeName,method); + if(customTypes.isEmpty()) { + System.err.println("UNACCEPTABLE0!!!!\n"); + } + return this; + } catch(NoSuchMethodException nsme) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a 'builder()' method"); + } catch(ClassCastException cce) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "': 'builder()' method must return an instance of Schema.Builder"); + } + } + /** * Registers a format validator with the name returned by {@link FormatValidator#formatName()}. * @@ -314,13 +356,17 @@ public SchemaLoader(SchemaLoaderBuilder builder) { } else { specVersion = builder.specVersion; } + if(!builder.customTypes.isEmpty()) { + System.err.println("ACCEPTABLE-1!!!!\n"); + } this.config = new LoaderConfig(builder.schemaClient, builder.formatValidators, builder.schemasByURI, specVersion, builder.useDefaults, builder.nullableSupport, - builder.regexpFactory); + builder.regexpFactory, + builder.customTypes); this.ls = new LoadingState(config, builder.pointerSchemas, effectiveRootSchemaJson, @@ -389,7 +435,7 @@ private AdjacentSchemaExtractionState runSchemaExtractors(JsonObject o) { new CombinedSchemaLoader(this), new NotSchemaExtractor(this), new ConstSchemaExtractor(this), - new TypeBasedSchemaExtractor(this), + new TypeBasedSchemaExtractor(this,config.customTypes), new PropertySnifferSchemaExtractor(this) ); for (SchemaExtractor extractor : extractors) { diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java new file mode 100644 index 000000000..9e6d6f763 --- /dev/null +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java @@ -0,0 +1,108 @@ +package org.everit.json.schema; + +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.commons.io.IOUtils; +import org.everit.json.schema.loader.SchemaLoader; +import org.everit.json.schema.Schema; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.Test; + + +import static java.util.Objects.requireNonNull; +import static org.everit.json.schema.FormatValidator.NONE; + +import java.util.Objects; + +import org.everit.json.schema.internal.JSONPrinter; +import org.everit.json.schema.regexp.JavaUtilRegexpFactory; +import org.everit.json.schema.regexp.Regexp; + +/** + * {@code String} schema validator. + */ +public class CustomTestSchema extends AbstractCustomTypeSchema { + + /** + * Builder class for {@link CustomTestSchema}. + */ + public static class SchemaLoaderBuilder extends Schema.Builder { + + private String rightValue; + + @Override + public CustomTestSchema build() { + return new CustomTestSchema(this); + } + + public SchemaLoaderBuilder rightValue(final String rightValue) { + this.rightValue = rightValue; + return this; + } + } + + public static SchemaLoaderBuilder builder() { + return new SchemaLoaderBuilder(); + } + + private final String rightValue; + + public CustomTestSchema() { + this(builder()); + } + + /** + * Constructor. + * + * @param builder + * the builder object containing validation criteria + */ + public CustomTestSchema(final SchemaLoaderBuilder builder) { + super(builder); + this.rightValue = builder.rightValue; + } + + @Override void accept(Visitor visitor) { + visitor.visitCustomTypeSchema(this); + } + + @Override + public Visitor buildVisitor(Object subject,ValidatingVisitor owner) { + return new CustomTypeSchemaValidatingVisitor(subject, owner); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o instanceof CustomTestSchema) { + CustomTestSchema that = (CustomTestSchema) o; + return that.canEqual(this) && + Objects.equals(rightValue, that.rightValue) && + super.equals(that); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), rightValue); + } + + @Override + protected boolean canEqual(Object other) { + return other instanceof CustomTestSchema; + } + + @Override + void describePropertiesTo(JSONPrinter writer) { + writer.ifPresent("rightValue", rightValue); + } + + public String rightValue() { + return rightValue; + } +} diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java new file mode 100644 index 000000000..22921752a --- /dev/null +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java @@ -0,0 +1,39 @@ +package org.everit.json.schema; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +import java.util.Optional; + +import org.everit.json.schema.regexp.Regexp; + +public class CustomTypeSchemaValidatingVisitor extends Visitor { + + private final Object subject; + + private final ValidatingVisitor owner; + + private CustomTestSchema customTestSchema; + + private String rightValueSubject; + + public CustomTypeSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) { + this.subject = subject; + this.owner = requireNonNull(owner, "failureReporter cannot be null"); + } + + void visitCustomTypeSchema(CustomTestSchema customTestSchema) { + this.customTestSchema = customTestSchema; + if (owner.passesTypeCheck(String.class, true, false)) { + rightValueSubject = (String) subject; + visitRightValue(customTestSchema.rightValue()); + } + } + + void visitRightValue(String rightValue) { + if(rightValue != null && !rightValueSubject.equals(rightValue)) { + ValidationException violation = new ValidationException(customTestSchema,"'"+rightValue+"' is not the right value ('"+rightValueSubject+"')"); + owner.failure(violation); + } + } +} diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java new file mode 100644 index 000000000..89b0f5fc6 --- /dev/null +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java @@ -0,0 +1,54 @@ +package org.everit.json.schema; + +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.commons.io.IOUtils; +import org.everit.json.schema.loader.SchemaLoader; +import org.everit.json.schema.Schema; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.junit.Test; + + +import static java.util.Objects.requireNonNull; +import static org.everit.json.schema.FormatValidator.NONE; + +import java.util.Objects; + +import org.everit.json.schema.internal.JSONPrinter; +import org.everit.json.schema.regexp.JavaUtilRegexpFactory; +import org.everit.json.schema.regexp.Regexp; + +public class CustomTypeTest { + + @Test + public void validateCustomType() throws IOException { + + JSONObject jsonSchema = new JSONObject(new JSONTokener( + IOUtils.toString( + new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/schema.json"))) + )); + + JSONObject worksJson = new JSONObject(new JSONTokener( + IOUtils.toString( + new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/works.json"))) + )); + + JSONObject failsJson = new JSONObject(new JSONTokener( + IOUtils.toString( + new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/fails.json"))) + )); + + SchemaLoader.SchemaLoaderBuilder loaderBuilder = SchemaLoader.builder(); + loaderBuilder.addCustomType("customType",CustomTestSchema.class); + //loaderBuilder.schemaJson(jsonSchema); + SchemaLoader sLoader = loaderBuilder.build(); + + Schema schema = sLoader.load(jsonSchema); + + schema.validate(worksJson); + schema.validate(failsJson); + } + +} diff --git a/tests/vanilla/src/main/resources/org/everit/json/schema/customType/fails.json b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/fails.json new file mode 100644 index 000000000..c16913a18 --- /dev/null +++ b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/fails.json @@ -0,0 +1,3 @@ +{ + "custom": "fortytwo" +} diff --git a/tests/vanilla/src/main/resources/org/everit/json/schema/customType/schema.json b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/schema.json new file mode 100644 index 000000000..7032b14ac --- /dev/null +++ b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/schema.json @@ -0,0 +1,10 @@ +{ + "title": "Schema with a custom type", + "type": "object", + "properties": { + "custom": { + "type": "customType", + "rightValue": "42" + } + } +} diff --git a/tests/vanilla/src/main/resources/org/everit/json/schema/customType/works.json b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/works.json new file mode 100644 index 000000000..cec9ff22b --- /dev/null +++ b/tests/vanilla/src/main/resources/org/everit/json/schema/customType/works.json @@ -0,0 +1,3 @@ +{ + "custom": "42" +} From 9ccd34661f1b5ff4e490207a773811ed73f192d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Mar=C3=ADa=20Fern=C3=A1ndez?= Date: Thu, 2 May 2019 17:24:04 +0200 Subject: [PATCH 2/5] Added a couple of methods easing the registration of custom types and removed some debug traces --- .../json/schema/loader/SchemaExtractor.java | 3 - .../json/schema/loader/SchemaLoader.java | 55 ++++++++++++++++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java index 8c544de29..275727ae6 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java @@ -283,9 +283,6 @@ private Schema.Builder loadForExplicitType(String typeString) { case "object": return buildObjectSchema(); default: - if(customTypes.isEmpty()) { - System.err.println("UNACCEPTABLE!!!!\n"); - } if(customTypes.containsKey(typeString)) { // Calling the public static builder method using the // Java reflection mechanisms diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java index 58f60261c..58d00c947 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java @@ -82,6 +82,17 @@ public SchemaLoaderBuilder() { setSpecVersion(DRAFT_4); } + /** + * Registers a custom schema type + * + * @param entry + * a Map.Entry with the typeName and the class to register + * @return {@code this} + */ + public SchemaLoaderBuilder addCustomType(Map.Entry> entry) { + return addCustomType(entry.getKey(),entry.getValue()); + } + /** * Registers a custom schema type * @@ -107,9 +118,6 @@ public SchemaLoaderBuilder addCustomType(String typeName,Class> customTypes) { + return SchemaLoader.load(schemaJson, new DefaultSchemaClient(), customTypes); + } + /** * Creates Schema instance from its JSON representation. * @@ -316,8 +337,29 @@ public static Schema load(final JSONObject schemaJson) { * @return the created schema */ public static Schema load(final JSONObject schemaJson, final SchemaClient schemaClient) { - SchemaLoader loader = builder() - .schemaJson(schemaJson) + return SchemaLoader.load(schemaJson,schemaClient,null); + } + + /** + * Creates Schema instance from its JSON representation. + * + * @param schemaJson + * the JSON representation of the schema. + * @param schemaClient + * the HTTP client to be used for resolving remote JSON references. + * @param customTypes + * the custom types to use on the validation process + * @return the created schema + */ + public static Schema load(final JSONObject schemaJson, final SchemaClient schemaClient, final Map> customTypes) { + SchemaLoaderBuilder builder = builder(); + if(customTypes != null) { + for(Map.Entry> customTypeP: customTypes.entrySet()) { + builder.addCustomType(customTypeP); + } + } + + SchemaLoader loader = builder.schemaJson(schemaJson) .schemaClient(schemaClient) .build(); return loader.load().build(); @@ -356,9 +398,6 @@ public SchemaLoader(SchemaLoaderBuilder builder) { } else { specVersion = builder.specVersion; } - if(!builder.customTypes.isEmpty()) { - System.err.println("ACCEPTABLE-1!!!!\n"); - } this.config = new LoaderConfig(builder.schemaClient, builder.formatValidators, builder.schemasByURI, From f12ee56d329f3ba30b9e468c312df83db1b03650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Mar=C3=ADa=20Fern=C3=A1ndez?= Date: Thu, 2 May 2019 17:26:14 +0200 Subject: [PATCH 3/5] (Cosmetic) Add documentation to AbstractCustomTypeSchema method --- .../everit/json/schema/AbstractCustomTypeSchema.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java index 4fe442f8b..86ef2423e 100644 --- a/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java +++ b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java @@ -16,6 +16,16 @@ protected AbstractCustomTypeSchema(Schema.Builder Date: Thu, 2 May 2019 17:27:46 +0200 Subject: [PATCH 4/5] Using new methods to validate --- .../java/org/everit/json/schema/CustomTypeTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java index 89b0f5fc6..c67e72c37 100644 --- a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java @@ -15,6 +15,7 @@ import static org.everit.json.schema.FormatValidator.NONE; import java.util.Objects; +import java.util.HashMap; import org.everit.json.schema.internal.JSONPrinter; import org.everit.json.schema.regexp.JavaUtilRegexpFactory; @@ -39,13 +40,11 @@ public void validateCustomType() throws IOException { IOUtils.toString( new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/fails.json"))) )); - - SchemaLoader.SchemaLoaderBuilder loaderBuilder = SchemaLoader.builder(); - loaderBuilder.addCustomType("customType",CustomTestSchema.class); - //loaderBuilder.schemaJson(jsonSchema); - SchemaLoader sLoader = loaderBuilder.build(); - Schema schema = sLoader.load(jsonSchema); + // An easy way to register the custom types + HashMap customTypes = new HashMap<>(); + customTypes.put("customType",CustomTestSchema.class); + Schema schema = SchemaLoader.load(jsonSchema,customTypes); schema.validate(worksJson); schema.validate(failsJson); From d5641e3f456a991d5523c9ee5eb6c1e3d8a2f36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Mar=C3=ADa=20Fern=C3=A1ndez?= Date: Fri, 3 May 2019 01:22:47 +0200 Subject: [PATCH 5/5] At last, the tests demonstrate that the custom types are being validated --- .../json/schema/AbstractCustomTypeSchema.java | 4 ++ .../json/schema/loader/LoaderConfig.java | 13 ++-- .../json/schema/loader/SchemaExtractor.java | 28 ++++++--- .../json/schema/loader/SchemaLoader.java | 59 ++++++++++++++----- .../everit/json/schema/CustomTestSchema.java | 19 +++--- .../CustomTypeSchemaValidatingVisitor.java | 8 +-- .../everit/json/schema/CustomTypeTest.java | 25 ++++++-- .../schema/loader/CustomTestSchemaLoader.java | 43 ++++++++++++++ 8 files changed, 154 insertions(+), 45 deletions(-) create mode 100644 tests/vanilla/src/main/java/org/everit/json/schema/loader/CustomTestSchemaLoader.java diff --git a/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java index 86ef2423e..eccb4141b 100644 --- a/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java +++ b/core/src/main/java/org/everit/json/schema/AbstractCustomTypeSchema.java @@ -29,4 +29,8 @@ protected AbstractCustomTypeSchema(Schema.Builder formatValidators; - final Map customTypes; + final Map customTypesMap; + + final Map> customTypesKeywordsMap; final Map schemasByURI; @@ -44,17 +47,19 @@ static LoaderConfig defaultV4Config() { LoaderConfig(SchemaClient schemaClient, Map formatValidators, SpecificationVersion specVersion, boolean useDefaults) { - this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory(), emptyMap()); + this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory(), emptyMap(), emptyMap()); } LoaderConfig(SchemaClient schemaClient, Map formatValidators, Map schemasByURI, SpecificationVersion specVersion, boolean useDefaults, boolean nullableSupport, RegexpFactory regexpFactory, - Map customTypes) { + Map customTypesMap, + Map> customTypesKeywordsMap) { this.schemaClient = requireNonNull(schemaClient, "schemaClient cannot be null"); this.formatValidators = requireNonNull(formatValidators, "formatValidators cannot be null"); - this.customTypes = requireNonNull(customTypes, "customTypes cannot be null"); + this.customTypesMap = requireNonNull(customTypesMap, "customTypesMap cannot be null"); + this.customTypesKeywordsMap = requireNonNull(customTypesKeywordsMap, "customTypesKeywordsMap cannot be null"); if (schemasByURI == null) { this.schemasByURI = new HashMap<>(); } else { diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java index 275727ae6..80e79f15b 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaExtractor.java @@ -100,7 +100,7 @@ abstract class AbstractSchemaExtractor implements SchemaExtractor { protected JsonObject schemaJson; - private KeyConsumer consumedKeys; + protected KeyConsumer consumedKeys; final SchemaLoader defaultLoader; @@ -240,11 +240,13 @@ private ConditionalSchema.Builder buildConditionalSchema() { } class TypeBasedSchemaExtractor extends AbstractSchemaExtractor { - private Map customTypes; + private Map customTypesMap; + private Map> customTypesKeywordsMap; - TypeBasedSchemaExtractor(SchemaLoader defaultLoader,Map customTypes) { + TypeBasedSchemaExtractor(SchemaLoader defaultLoader, Map customTypesMap, Map> customTypesKeywordsMap) { super(defaultLoader); - this.customTypes = customTypes; + this.customTypesMap = customTypesMap; + this.customTypesKeywordsMap = customTypesKeywordsMap; } @Override List> extract() { @@ -283,12 +285,24 @@ private Schema.Builder loadForExplicitType(String typeString) { case "object": return buildObjectSchema(); default: - if(customTypes.containsKey(typeString)) { + if(customTypesMap.containsKey(typeString)) { // Calling the public static builder method using the // Java reflection mechanisms - Method builderMethod = customTypes.get(typeString); + Method builderMethod = customTypesMap.get(typeString); + if(builderMethod==null) { + throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("type: [%s] builder creation has failed, as type was not found", typeString)); + } + + List typeKeywords = customTypesKeywordsMap.get(typeString); + if(typeKeywords==null) { + throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("type: [%s] builder creation has failed, as type was not found", typeString)); + } try { - return (Schema.Builder) builderMethod.invoke(null); + // Register the listened keywords + typeKeywords.forEach(consumedKeys::keyConsumed); + + // Now, obtain the schema loader + return (Schema.Builder) builderMethod.invoke(null,schemaJson.ls, config(), defaultLoader); } catch(InvocationTargetException ite) { throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("type: [%s] builder creation has failed", typeString)); } catch(IllegalAccessException iae) { diff --git a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java index 58d00c947..b7ac39c5c 100644 --- a/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java +++ b/core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java @@ -9,6 +9,7 @@ import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_6; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -74,7 +75,8 @@ public static class SchemaLoaderBuilder { RegexpFactory regexpFactory = new JavaUtilRegexpFactory(); - Map customTypes = new HashMap<>(); + Map customTypesMap = new HashMap<>(); + Map> customTypesKeywordsMap = new HashMap<>(); Map schemasByURI = null; @@ -89,7 +91,7 @@ public SchemaLoaderBuilder() { * a Map.Entry with the typeName and the class to register * @return {@code this} */ - public SchemaLoaderBuilder addCustomType(Map.Entry> entry) { + public SchemaLoaderBuilder addCustomType(Map.Entry> entry) { return addCustomType(entry.getKey(),entry.getValue()); } @@ -102,28 +104,54 @@ public SchemaLoaderBuilder addCustomType(Map.Entry clazz) { + public SchemaLoaderBuilder addCustomType(String typeName,Class clazz) { typeName = requireNonNull(typeName, "the name of the custom type cannot be null"); if(typeName.length() == 0) { throw new IllegalArgumentException("the name of the custom type must be non-empty"); } + + // Checking the pre-conditions + Method method = null; try { - Method method = clazz.getMethod("builder"); + method = clazz.getMethod("schemaBuilderLoader", LoadingState.class, LoaderConfig.class, SchemaLoader.class); int mods = method.getModifiers(); if(!Modifier.isStatic(mods) || !Modifier.isPublic(mods)) { - throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a public static 'builder()' method"); + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a public static 'schemaBuilderLoader(LoadingState ls, LoaderConfig config, SchemaLoader defaultLoader)' method"); } Class retClazz = method.getReturnType(); retClazz.asSubclass(Schema.Builder.class); + } catch(NoSuchMethodException nsme) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a 'schemaBuilderLoader(LoadingState ls, LoaderConfig config, SchemaLoader defaultLoader)' method"); + } catch(ClassCastException cce) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "': 'schemaBuilderLoader(LoadingState ls, LoaderConfig config, SchemaLoader defaultLoader)' method must return an instance of Schema.Builder"); + } + + List customTypeKeywords = null; + try { + Method kwMethod = clazz.getMethod("schemaKeywords"); + int mods = method.getModifiers(); + if(!Modifier.isStatic(mods) || !Modifier.isPublic(mods)) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a public static 'schemaKeywords()' method"); + } + Class retClazz = kwMethod.getReturnType(); + retClazz.asSubclass(List.class); - // If all is ok - customTypes.put(typeName,method); - return this; + // Now, obtain the list + customTypeKeywords = (List)kwMethod.invoke(null); } catch(NoSuchMethodException nsme) { - throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a 'builder()' method"); + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' must have a 'schemaKeywords()' method"); } catch(ClassCastException cce) { - throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "': 'builder()' method must return an instance of Schema.Builder"); + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "': 'schemaKeywords()' method must return an instance of List"); + } catch(InvocationTargetException ite) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' failed invoking 'schemaKeywords()' method"); + } catch(IllegalAccessException iae) { + throw new IllegalArgumentException("class '" + clazz.getName() + "', manager of custom type '" + typeName + "' failed invoking 'schemaKeywords()' method"); } + + // If we are here, all is ok + customTypesMap.put(typeName,method); + customTypesKeywordsMap.put(typeName,customTypeKeywords); + return this; } /** @@ -323,7 +351,7 @@ public static Schema load(final JSONObject schemaJson) { * the custom types to use on the validation process * @return the created schema */ - public static Schema load(final JSONObject schemaJson, final Map> customTypes) { + public static Schema load(final JSONObject schemaJson, final Map> customTypes) { return SchemaLoader.load(schemaJson, new DefaultSchemaClient(), customTypes); } @@ -351,10 +379,10 @@ public static Schema load(final JSONObject schemaJson, final SchemaClient schema * the custom types to use on the validation process * @return the created schema */ - public static Schema load(final JSONObject schemaJson, final SchemaClient schemaClient, final Map> customTypes) { + public static Schema load(final JSONObject schemaJson, final SchemaClient schemaClient, final Map> customTypes) { SchemaLoaderBuilder builder = builder(); if(customTypes != null) { - for(Map.Entry> customTypeP: customTypes.entrySet()) { + for(Map.Entry> customTypeP: customTypes.entrySet()) { builder.addCustomType(customTypeP); } } @@ -405,7 +433,8 @@ public SchemaLoader(SchemaLoaderBuilder builder) { builder.useDefaults, builder.nullableSupport, builder.regexpFactory, - builder.customTypes); + builder.customTypesMap, + builder.customTypesKeywordsMap); this.ls = new LoadingState(config, builder.pointerSchemas, effectiveRootSchemaJson, @@ -474,7 +503,7 @@ private AdjacentSchemaExtractionState runSchemaExtractors(JsonObject o) { new CombinedSchemaLoader(this), new NotSchemaExtractor(this), new ConstSchemaExtractor(this), - new TypeBasedSchemaExtractor(this,config.customTypes), + new TypeBasedSchemaExtractor(this,config.customTypesMap,config.customTypesKeywordsMap), new PropertySnifferSchemaExtractor(this) ); for (SchemaExtractor extractor : extractors) { diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java index 9e6d6f763..e3e826556 100644 --- a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTestSchema.java @@ -2,9 +2,9 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.util.List; import org.apache.commons.io.IOUtils; -import org.everit.json.schema.loader.SchemaLoader; import org.everit.json.schema.Schema; import org.json.JSONObject; import org.json.JSONTokener; @@ -21,6 +21,7 @@ import org.everit.json.schema.regexp.Regexp; /** + * @author jmfernandez * {@code String} schema validator. */ public class CustomTestSchema extends AbstractCustomTypeSchema { @@ -28,7 +29,7 @@ public class CustomTestSchema extends AbstractCustomTypeSchema { /** * Builder class for {@link CustomTestSchema}. */ - public static class SchemaLoaderBuilder extends Schema.Builder { + public static class Builder extends Schema.Builder { private String rightValue; @@ -37,16 +38,16 @@ public CustomTestSchema build() { return new CustomTestSchema(this); } - public SchemaLoaderBuilder rightValue(final String rightValue) { + public Builder rightValue(final String rightValue) { this.rightValue = rightValue; return this; } } - public static SchemaLoaderBuilder builder() { - return new SchemaLoaderBuilder(); + public static Builder builder() { + return new Builder(); } - + private final String rightValue; public CustomTestSchema() { @@ -59,15 +60,11 @@ public CustomTestSchema() { * @param builder * the builder object containing validation criteria */ - public CustomTestSchema(final SchemaLoaderBuilder builder) { + public CustomTestSchema(final Builder builder) { super(builder); this.rightValue = builder.rightValue; } - @Override void accept(Visitor visitor) { - visitor.visitCustomTypeSchema(this); - } - @Override public Visitor buildVisitor(Object subject,ValidatingVisitor owner) { return new CustomTypeSchemaValidatingVisitor(subject, owner); diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java index 22921752a..8f0dedd2e 100644 --- a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeSchemaValidatingVisitor.java @@ -22,17 +22,17 @@ public CustomTypeSchemaValidatingVisitor(Object subject, ValidatingVisitor owner this.owner = requireNonNull(owner, "failureReporter cannot be null"); } - void visitCustomTypeSchema(CustomTestSchema customTestSchema) { - this.customTestSchema = customTestSchema; + void visitCustomTypeSchema(AbstractCustomTypeSchema customTestSchema) { + this.customTestSchema = (CustomTestSchema)customTestSchema; if (owner.passesTypeCheck(String.class, true, false)) { rightValueSubject = (String) subject; - visitRightValue(customTestSchema.rightValue()); + visitRightValue(this.customTestSchema.rightValue()); } } void visitRightValue(String rightValue) { if(rightValue != null && !rightValueSubject.equals(rightValue)) { - ValidationException violation = new ValidationException(customTestSchema,"'"+rightValue+"' is not the right value ('"+rightValueSubject+"')"); + ValidationException violation = new ValidationException(customTestSchema,"'"+rightValueSubject+"' is not the right value ('"+rightValue+"')"); owner.failure(violation); } } diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java index c67e72c37..eeb30efe3 100644 --- a/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java +++ b/tests/vanilla/src/main/java/org/everit/json/schema/CustomTypeTest.java @@ -4,8 +4,10 @@ import java.io.InputStreamReader; import org.apache.commons.io.IOUtils; +import org.everit.json.schema.loader.CustomTestSchemaLoader; import org.everit.json.schema.loader.SchemaLoader; import org.everit.json.schema.Schema; +import org.everit.json.schema.ValidationException; import org.json.JSONObject; import org.json.JSONTokener; import org.junit.Test; @@ -35,6 +37,23 @@ public void validateCustomType() throws IOException { IOUtils.toString( new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/works.json"))) )); + + // An easy way to register the custom types + HashMap> customTypes = new HashMap<>(); + customTypes.put("customType",CustomTestSchemaLoader.class); + Schema schema = SchemaLoader.load(jsonSchema,customTypes); + + schema.validate(worksJson); + } + + + @Test(expected = ValidationException.class) + public void dontValidateCustomType() throws IOException { + + JSONObject jsonSchema = new JSONObject(new JSONTokener( + IOUtils.toString( + new InputStreamReader(getClass().getResourceAsStream("/org/everit/json/schema/customType/schema.json"))) + )); JSONObject failsJson = new JSONObject(new JSONTokener( IOUtils.toString( @@ -42,12 +61,10 @@ public void validateCustomType() throws IOException { )); // An easy way to register the custom types - HashMap customTypes = new HashMap<>(); - customTypes.put("customType",CustomTestSchema.class); + HashMap> customTypes = new HashMap<>(); + customTypes.put("customType",CustomTestSchemaLoader.class); Schema schema = SchemaLoader.load(jsonSchema,customTypes); - schema.validate(worksJson); schema.validate(failsJson); } - } diff --git a/tests/vanilla/src/main/java/org/everit/json/schema/loader/CustomTestSchemaLoader.java b/tests/vanilla/src/main/java/org/everit/json/schema/loader/CustomTestSchemaLoader.java new file mode 100644 index 000000000..47e66bbeb --- /dev/null +++ b/tests/vanilla/src/main/java/org/everit/json/schema/loader/CustomTestSchemaLoader.java @@ -0,0 +1,43 @@ +package org.everit.json.schema.loader; + +import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import org.everit.json.schema.CustomTestSchema; +import org.everit.json.schema.Schema; + +/** + * @author jmfernandez + */ +public class CustomTestSchemaLoader { + static final List SCHEMA_PROPS = asList("rightValue"); + + private final LoadingState ls; + + private final LoaderConfig config; + + private final SchemaLoader defaultLoader; + + public CustomTestSchemaLoader(LoadingState ls, LoaderConfig config, SchemaLoader defaultLoader) { + this.ls = requireNonNull(ls, "ls cannot be null"); + this.config = requireNonNull(config, "config cannot be null"); + this.defaultLoader = requireNonNull(defaultLoader, "defaultLoader cannot be null"); + } + + CustomTestSchema.Builder load() { + CustomTestSchema.Builder builder = CustomTestSchema.builder(); + ls.schemaJson().maybe("rightValue").map(JsonValue::requireString).ifPresent(builder::rightValue); + return builder; + } + + // This method is + public static CustomTestSchema.Builder schemaBuilderLoader(LoadingState ls, LoaderConfig config, SchemaLoader schemaLoader) { + return new CustomTestSchemaLoader(ls, config, schemaLoader).load(); + } + + public static final List schemaKeywords() { + return CustomTestSchemaLoader.SCHEMA_PROPS; + } +}