package org.everit.json.schema.loader; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_4; import static org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import org.everit.json.schema.ArraySchema; import org.everit.json.schema.BooleanSchema; import org.everit.json.schema.CombinedSchema; import org.everit.json.schema.ConditionalSchema; import org.everit.json.schema.ConstSchema; import org.everit.json.schema.EnumSchema; import org.everit.json.schema.NotSchema; import org.everit.json.schema.NullSchema; import org.everit.json.schema.NumberSchema; import org.everit.json.schema.ObjectSchema; import org.everit.json.schema.Schema; import org.everit.json.schema.SchemaException; import org.everit.json.schema.StringSchema; class KeyConsumer { private final Set<String> consumedKeys; private final JsonObject schemaJson; KeyConsumer(JsonObject schemaJson) { this.schemaJson = schemaJson; this.consumedKeys = new HashSet<>(schemaJson.keySet().size()); } void keyConsumed(String key) { if (schemaJson.keySet().contains(key)) { consumedKeys.add(key); } } JsonValue require(String key) { keyConsumed(key); return schemaJson.require(key); } Optional<JsonValue> maybe(String key) { keyConsumed(key); return schemaJson.maybe(key); } public Set<String> collect() { return consumedKeys; } } class ExtractionResult { final Set<String> consumedKeys; final Collection<Schema.Builder<?>> extractedSchemas; ExtractionResult(Set<String> consumedKeys, Collection<Schema.Builder<?>> extractedSchemas) { this.consumedKeys = requireNonNull(consumedKeys, "consumedKeys cannot be null"); this.extractedSchemas = requireNonNull(extractedSchemas, "extractedSchemas cannot be null"); } ExtractionResult(String consumedKeys, Collection<Schema.Builder<?>> extactedSchemas) { this(singleton(consumedKeys), extactedSchemas); } } interface SchemaExtractor { ExtractionResult extract(JsonObject schemaJson); } abstract class AbstractSchemaExtractor implements SchemaExtractor { static final List<String> NUMBER_SCHEMA_PROPS = asList("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"); static final List<String> STRING_SCHEMA_PROPS = asList("minLength", "maxLength", "pattern", "format"); protected JsonObject schemaJson; private KeyConsumer consumedKeys; final SchemaLoader defaultLoader; private ExclusiveLimitHandler exclusiveLimitHandler; AbstractSchemaExtractor(SchemaLoader defaultLoader) { this.defaultLoader = requireNonNull(defaultLoader, "defaultLoader cannot be null"); } @Override public final ExtractionResult extract(JsonObject schemaJson) { this.schemaJson = requireNonNull(schemaJson, "schemaJson cannot be null"); this.exclusiveLimitHandler = ExclusiveLimitHandler.ofSpecVersion(config().specVersion); consumedKeys = new KeyConsumer(schemaJson); return new ExtractionResult(consumedKeys.collect(), extract()); } JsonValue require(String key) { return consumedKeys.require(key); } Optional<JsonValue> maybe(String key) { return consumedKeys.maybe(key); } boolean containsKey(String key) { return schemaJson.containsKey(key); } boolean schemaHasAnyOf(Collection<String> propNames) { return propNames.stream().anyMatch(schemaJson::containsKey); } LoaderConfig config() { return schemaJson.ls.config; } ObjectSchema.Builder buildObjectSchema() { config().specVersion.objectKeywords().forEach(consumedKeys::keyConsumed); return new ObjectSchemaLoader(schemaJson.ls, config(), defaultLoader).load(); } ArraySchema.Builder buildArraySchema() { config().specVersion.arrayKeywords().forEach(consumedKeys::keyConsumed); return new ArraySchemaLoader(schemaJson.ls, config(), defaultLoader).load(); } NumberSchema.Builder buildNumberSchema() { PropertySnifferSchemaExtractor.NUMBER_SCHEMA_PROPS.forEach(consumedKeys::keyConsumed); NumberSchema.Builder builder = NumberSchema.builder(); maybe("minimum").map(JsonValue::requireNumber).ifPresent(builder::minimum); maybe("maximum").map(JsonValue::requireNumber).ifPresent(builder::maximum); maybe("multipleOf").map(JsonValue::requireNumber).ifPresent(builder::multipleOf); maybe("exclusiveMinimum").ifPresent(exclMin -> exclusiveLimitHandler.handleExclusiveMinimum(exclMin, builder)); maybe("exclusiveMaximum").ifPresent(exclMax -> exclusiveLimitHandler.handleExclusiveMaximum(exclMax, builder)); return builder; } StringSchema.Builder buildStringSchema() { PropertySnifferSchemaExtractor.STRING_SCHEMA_PROPS.forEach(consumedKeys::keyConsumed); return new StringSchemaLoader(schemaJson.ls, config().formatValidators).load(); } abstract List<Schema.Builder<?>> extract(); } class EnumSchemaExtractor extends AbstractSchemaExtractor { EnumSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { if (!containsKey("enum")) { return emptyList(); } EnumSchema.Builder builder = EnumSchema.builder(); List<Object> possibleValues = new ArrayList<>(); require("enum").requireArray().forEach((i, item) -> possibleValues.add(item.unwrap())); builder.possibleValues(possibleValues); return singletonList(builder); } } class ReferenceSchemaExtractor extends AbstractSchemaExtractor { ReferenceSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { if (containsKey("$ref")) { String ref = require("$ref").requireString(); return singletonList(new ReferenceLookup(schemaJson.ls).lookup(ref, schemaJson)); } return emptyList(); } } class PropertySnifferSchemaExtractor extends AbstractSchemaExtractor { static final List<String> CONDITIONAL_SCHEMA_KEYWORDS = asList("if", "then", "else"); PropertySnifferSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { List<Schema.Builder<?>> builders = new ArrayList<>(1); if (schemaHasAnyOf(config().specVersion.arrayKeywords())) { builders.add(buildArraySchema().requiresArray(false)); } if (schemaHasAnyOf(config().specVersion.objectKeywords())) { builders.add(buildObjectSchema().requiresObject(false)); } if (schemaHasAnyOf(NUMBER_SCHEMA_PROPS)) { builders.add(buildNumberSchema().requiresNumber(false)); } if (schemaHasAnyOf(STRING_SCHEMA_PROPS)) { builders.add(buildStringSchema().requiresString(false)); } if (config().specVersion.isAtLeast(DRAFT_7) && schemaHasAnyOf(CONDITIONAL_SCHEMA_KEYWORDS)) { builders.add(buildConditionalSchema()); } return builders; } private ConditionalSchema.Builder buildConditionalSchema() { ConditionalSchema.Builder builder = ConditionalSchema.builder(); maybe("if").map(defaultLoader::loadChild).map(Schema.Builder::build).ifPresent(builder::ifSchema); maybe("then").map(defaultLoader::loadChild).map(Schema.Builder::build).ifPresent(builder::thenSchema); maybe("else").map(defaultLoader::loadChild).map(Schema.Builder::build).ifPresent(builder::elseSchema); return builder; } } class TypeBasedSchemaExtractor extends AbstractSchemaExtractor { TypeBasedSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { if (containsKey("type")) { return singletonList(require("type").canBeMappedTo(JsonArray.class, arr -> (Schema.Builder) buildAnyOfSchemaForMultipleTypes()) .orMappedTo(String.class, this::loadForExplicitType) .requireAny()); } else { return emptyList(); } } private CombinedSchema.Builder buildAnyOfSchemaForMultipleTypes() { JsonArray subtypeJsons = require("type").requireArray(); Collection<Schema> subschemas = new ArrayList<>(subtypeJsons.length()); subtypeJsons.forEach((j, raw) -> { subschemas.add(loadForExplicitType(raw.requireString()).build()); }); return CombinedSchema.anyOf(subschemas); } private Schema.Builder<?> loadForExplicitType(String typeString) { switch (typeString) { case "string": return buildStringSchema().requiresString(true); case "integer": return buildNumberSchema().requiresInteger(true); case "number": return buildNumberSchema(); case "boolean": return BooleanSchema.builder(); case "null": return NullSchema.builder(); case "array": return buildArraySchema(); case "object": return buildObjectSchema(); default: throw new SchemaException(schemaJson.ls.locationOfCurrentObj(), format("unknown type: [%s]", typeString)); } } } class NotSchemaExtractor extends AbstractSchemaExtractor { NotSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { if (containsKey("not")) { Schema mustNotMatch = defaultLoader.loadChild(require("not")).build(); return singletonList(NotSchema.builder().mustNotMatch(mustNotMatch)); } return emptyList(); } } class ConstSchemaExtractor extends AbstractSchemaExtractor { ConstSchemaExtractor(SchemaLoader defaultLoader) { super(defaultLoader); } @Override List<Schema.Builder<?>> extract() { if (config().specVersion != DRAFT_4 && containsKey("const")) { return singletonList(ConstSchema.builder().permittedValue(require("const").unwrap())); } else { return emptyList(); } } }