Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom types support #296

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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<? extends AbstractCustomTypeSchema> builder) {
super(builder);
}

/**
* On custom types, it should return an instance of its own visitor implementation
*
* @param subject
* the subject/context of the new visitor
*
* @param owner
* the owner of the new visitor
*
* @return
* The newly created Visitor for this custom type
*
*/
public abstract Visitor buildVisitor(Object subject,ValidatingVisitor owner);

@Override void accept(Visitor visitor) {
visitor.visitCustomTypeSchema(this);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schema> subschemas = combinedSchema.getSubschemas();
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/everit/json/schema/Visitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ void visitStringSchema(StringSchema stringSchema) {
visitFormat(stringSchema.getFormatValidator());
}

void visitCustomTypeSchema(AbstractCustomTypeSchema customTypeSchema) {
}

void visitFormat(FormatValidator formatValidator) {
}

Expand Down
15 changes: 13 additions & 2 deletions core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
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.List;
import java.util.Map;

import org.everit.json.schema.FormatValidator;
Expand All @@ -27,6 +30,10 @@ static LoaderConfig defaultV4Config() {
final SchemaClient schemaClient;

final Map<String, FormatValidator> formatValidators;

final Map<String, Method> customTypesMap;

final Map<String, List<String>> customTypesKeywordsMap;

final Map<URI, Object> schemasByURI;

Expand All @@ -40,15 +47,19 @@ static LoaderConfig defaultV4Config() {

LoaderConfig(SchemaClient schemaClient, Map<String, FormatValidator> formatValidators,
SpecificationVersion specVersion, boolean useDefaults) {
this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory());
this(schemaClient, formatValidators, emptyMap(), specVersion, useDefaults, false, new JavaUtilRegexpFactory(), emptyMap(), emptyMap());
}

LoaderConfig(SchemaClient schemaClient, Map<String, FormatValidator> formatValidators,
Map<URI, Object> schemasByURI,
SpecificationVersion specVersion, boolean useDefaults, boolean nullableSupport,
RegexpFactory regexpFactory) {
RegexpFactory regexpFactory,
Map<String,Method> customTypesMap,
Map<String,List<String>> customTypesKeywordsMap) {
this.schemaClient = requireNonNull(schemaClient, "schemaClient cannot be null");
this.formatValidators = requireNonNull(formatValidators, "formatValidators 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -95,7 +100,7 @@ abstract class AbstractSchemaExtractor implements SchemaExtractor {

protected JsonObject schemaJson;

private KeyConsumer consumedKeys;
protected KeyConsumer consumedKeys;

final SchemaLoader defaultLoader;

Expand Down Expand Up @@ -235,9 +240,13 @@ private ConditionalSchema.Builder buildConditionalSchema() {
}

class TypeBasedSchemaExtractor extends AbstractSchemaExtractor {
private Map<String,Method> customTypesMap;
private Map<String,List<String>> customTypesKeywordsMap;

TypeBasedSchemaExtractor(SchemaLoader defaultLoader) {
TypeBasedSchemaExtractor(SchemaLoader defaultLoader, Map<String,Method> customTypesMap, Map<String,List<String>> customTypesKeywordsMap) {
super(defaultLoader);
this.customTypesMap = customTypesMap;
this.customTypesKeywordsMap = customTypesKeywordsMap;
}

@Override List<Schema.Builder<?>> extract() {
Expand Down Expand Up @@ -276,7 +285,32 @@ private Schema.Builder<?> loadForExplicitType(String typeString) {
case "object":
return buildObjectSchema();
default:
throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("unknown type: [%s]", typeString));
if(customTypesMap.containsKey(typeString)) {
// Calling the public static builder method using the
// Java reflection mechanisms
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<String> 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 {
// Register the listened keywords
typeKeywords.forEach(consumedKeys::keyConsumed);

// Now, obtain the schema loader
return (Schema.Builder<? extends AbstractCustomTypeSchema>) 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) {
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));
}
}
}

Expand Down
124 changes: 119 additions & 5 deletions core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
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;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
Expand All @@ -18,6 +22,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;
Expand Down Expand Up @@ -69,13 +74,86 @@ public static class SchemaLoaderBuilder {
private boolean nullableSupport = false;

RegexpFactory regexpFactory = new JavaUtilRegexpFactory();

Map<String,Method> customTypesMap = new HashMap<>();
Map<String,List<String>> customTypesKeywordsMap = new HashMap<>();

Map<URI, Object> schemasByURI = null;

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<String,Class<?>> entry) {
return addCustomType(entry.getKey(),entry.getValue());
}

/**
* 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");
}

// Checking the pre-conditions
Method method = null;
try {
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 '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<String> 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);

// Now, obtain the list
customTypeKeywords = (List<String>)kwMethod.invoke(null);
} catch(NoSuchMethodException nsme) {
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 + "': 'schemaKeywords()' method must return an instance of List<String>");
} 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;
}

/**
* Registers a format validator with the name returned by {@link FormatValidator#formatName()}.
*
Expand Down Expand Up @@ -264,6 +342,19 @@ public static Schema load(final JSONObject schemaJson) {
return SchemaLoader.load(schemaJson, new DefaultSchemaClient());
}

/**
* Creates Schema instance from its JSON representation.
*
* @param schemaJson
* the JSON representation of the schema.
* @param customTypes
* the custom types to use on the validation process
* @return the created schema
*/
public static Schema load(final JSONObject schemaJson, final Map<String,Class<?>> customTypes) {
return SchemaLoader.load(schemaJson, new DefaultSchemaClient(), customTypes);
}

/**
* Creates Schema instance from its JSON representation.
*
Expand All @@ -274,8 +365,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<String,Class<?>> customTypes) {
SchemaLoaderBuilder builder = builder();
if(customTypes != null) {
for(Map.Entry<String,Class<?>> customTypeP: customTypes.entrySet()) {
builder.addCustomType(customTypeP);
}
}

SchemaLoader loader = builder.schemaJson(schemaJson)
.schemaClient(schemaClient)
.build();
return loader.load().build();
Expand Down Expand Up @@ -320,7 +432,9 @@ public SchemaLoader(SchemaLoaderBuilder builder) {
specVersion,
builder.useDefaults,
builder.nullableSupport,
builder.regexpFactory);
builder.regexpFactory,
builder.customTypesMap,
builder.customTypesKeywordsMap);
this.ls = new LoadingState(config,
builder.pointerSchemas,
effectiveRootSchemaJson,
Expand Down Expand Up @@ -389,7 +503,7 @@ private AdjacentSchemaExtractionState runSchemaExtractors(JsonObject o) {
new CombinedSchemaLoader(this),
new NotSchemaExtractor(this),
new ConstSchemaExtractor(this),
new TypeBasedSchemaExtractor(this),
new TypeBasedSchemaExtractor(this,config.customTypesMap,config.customTypesKeywordsMap),
new PropertySnifferSchemaExtractor(this)
);
for (SchemaExtractor extractor : extractors) {
Expand Down