All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dimajix.shaded.everit.schema.loader.SchemaLoader Maven / Gradle / Ivy

There is a newer version: 1.2.0-synapse3.3-spark3.3-hadoop3.3
Show newest version
package com.dimajix.shaded.everit.schema.loader;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static com.dimajix.shaded.everit.schema.loader.OrgJsonUtil.toMap;
import static com.dimajix.shaded.everit.schema.loader.SpecificationVersion.DRAFT_4;
import static com.dimajix.shaded.everit.schema.loader.SpecificationVersion.DRAFT_6;
import static com.dimajix.shaded.everit.schema.loader.SpecificationVersion.DRAFT_7;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;

import com.dimajix.shaded.everit.schema.CombinedSchema;
import com.dimajix.shaded.everit.schema.EmptySchema;
import com.dimajix.shaded.everit.schema.FalseSchema;
import com.dimajix.shaded.everit.schema.FormatValidator;
import com.dimajix.shaded.everit.schema.Schema;
import com.dimajix.shaded.everit.schema.SchemaException;
import com.dimajix.shaded.everit.schema.SchemaLocation;
import com.dimajix.shaded.everit.schema.TrueSchema;
import com.dimajix.shaded.everit.schema.loader.internal.DefaultSchemaClient;
import com.dimajix.shaded.everit.schema.loader.internal.WrappingFormatValidator;
import com.dimajix.shaded.everit.schema.regexp.JavaUtilRegexpFactory;
import com.dimajix.shaded.everit.schema.regexp.RegexpFactory;
import com.dimajix.shaded.json.JSONObject;

/**
 * Loads a JSON schema's JSON representation into schema validator instances.
 */
public class SchemaLoader {

    static JSONObject toOrgJSONObject(JsonObject value) {
        return new JSONObject(value.toMap());
    }

    /**
     * Builder class for {@link SchemaLoader}.
     */
    public static class SchemaLoaderBuilder {

        SchemaClient schemaClient = new DefaultSchemaClient();

        Object schemaJson;

        Object rootSchemaJson;

        Map pointerSchemas = new HashMap<>();

        Map subschemaRegistries = new HashMap<>();

        URI id;

        SchemaLocation pointerToCurrentObj = SchemaLocation.empty();

        Map formatValidators = new HashMap<>();

        SpecificationVersion specVersion;

        private boolean specVersionIsExplicitlySet = false;

        boolean useDefaults = false;

        private boolean nullableSupport = false;

        RegexpFactory regexpFactory = new JavaUtilRegexpFactory();

        Map schemasByURI = null;

        private boolean enableOverrideOfBuiltInFormatValidators;

        public SchemaLoaderBuilder() {
            setSpecVersion(DRAFT_4);
        }

        /**
         * Registers a format validator with the name returned by {@link FormatValidator#formatName()}.
         *
         * @param formatValidator
         *         the format validator to be registered with its name
         * @return {@code this}
         */
        public SchemaLoaderBuilder addFormatValidator(FormatValidator formatValidator) {
            formatValidators.put(formatValidator.formatName(), formatValidator);
            return this;
        }

        /**
         * @param formatName
         *         the name which will be used in the schema JSON files to refer to this {@code formatValidator}
         * @param formatValidator
         *         the object performing the validation for schemas which use the {@code formatName} format
         * @return {@code this}
         * @deprecated instead it is better to override {@link FormatValidator#formatName()}
         * and use {@link #addFormatValidator(FormatValidator)}
         */
        @Deprecated
        public SchemaLoaderBuilder addFormatValidator(String formatName,
                final FormatValidator formatValidator) {
            if (!Objects.equals(formatName, formatValidator.formatName())) {
                formatValidators.put(formatName, new WrappingFormatValidator(formatName, formatValidator));
            } else {
                formatValidators.put(formatName, formatValidator);
            }
            return this;
        }

        public SchemaLoaderBuilder draftV6Support() {
            setSpecVersion(DRAFT_6);
            specVersionIsExplicitlySet = true;
            return this;
        }

        public SchemaLoaderBuilder draftV7Support() {
            setSpecVersion(DRAFT_7);
            specVersionIsExplicitlySet = true;
            return this;
        }

        private void setSpecVersion(SpecificationVersion specVersion) {
            this.specVersion = specVersion;
        }

        private Optional specVersionInSchema() {
            Optional specVersion = Optional.empty();
            if (schemaJson instanceof Map) {
                Map schemaObj = (Map) schemaJson;
                String metaSchemaURL = (String) schemaObj.get("$schema");
                try {
                    specVersion = Optional.ofNullable(metaSchemaURL).map((SpecificationVersion::getByMetaSchemaUrl));
                } catch (IllegalArgumentException e) {
                    return specVersion;
                }
            }
            return specVersion;
        }

        public SchemaLoader build() {
            specVersionInSchema().ifPresent(this::setSpecVersion);
            addBuiltInFormatValidators();
            return new SchemaLoader(this);
        }

        private void addBuiltInFormatValidators() {
            Map defaultFormatValidators = specVersion.defaultFormatValidators();

            if (enableOverrideOfBuiltInFormatValidators) {
                for (Entry entry : defaultFormatValidators.entrySet()) {
                    formatValidators.putIfAbsent(entry.getKey(), entry.getValue());
                }
            } else {
                formatValidators.putAll(defaultFormatValidators);
            }
        }
        @Deprecated
        public JSONObject getRootSchemaJson() {
            return new JSONObject((Map) (rootSchemaJson == null ? schemaJson : rootSchemaJson));
        }

        /**
         * @deprecated use {@link #schemaClient(SchemaClient)} instead
         */
        @Deprecated
        public SchemaLoaderBuilder httpClient(SchemaClient httpClient) {
            this.schemaClient = httpClient;
            return this;
        }

        public SchemaLoaderBuilder schemaClient(SchemaClient schemaClient) {
            this.schemaClient = schemaClient;
            return this;
        }

        /**
         * Sets the initial resolution scope of the schema. {@code id} and {@code $ref} attributes
         * accuring in the schema will be resolved against this value.
         *
         * @param id
         *         the initial (absolute) URI, used as the resolution scope.
         * @return {@code this}
         */
        public SchemaLoaderBuilder resolutionScope(String id) {
            try {
                return resolutionScope(new URI(id));
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
        }

        public SchemaLoaderBuilder resolutionScope(URI id) {
            this.id = id;
            this.pointerToCurrentObj = new SchemaLocation(id, emptyList());
            return this;
        }

        SchemaLoaderBuilder pointerSchemas(Map pointerSchemas) {
            this.pointerSchemas = pointerSchemas;
            return this;
        }

        SchemaLoaderBuilder subschemaRegistries(Map subschemaRegistries) {
            this.subschemaRegistries = subschemaRegistries;
            return this;
        }

        SchemaLoaderBuilder rootSchemaJson(Object rootSchemaJson) {
            this.rootSchemaJson = rootSchemaJson;
            return this;
        }

        public SchemaLoaderBuilder schemaJson(JSONObject schemaJson) {
            return schemaJson(toMap(schemaJson));
        }

        public SchemaLoaderBuilder schemaJson(Object schema) {
            if (schema instanceof JSONObject) {
                schema = toMap((JSONObject) schema);
            }
            this.schemaJson = schema;
            return this;
        }

        SchemaLoaderBuilder formatValidators(Map formatValidators) {
            this.formatValidators = formatValidators;
            return this;
        }

        SchemaLoaderBuilder pointerToCurrentObj(SchemaLocation pointerToCurrentObj) {
            this.pointerToCurrentObj = requireNonNull(pointerToCurrentObj);
            return this;
        }

        /**
         * With this flag set to false, the validator ignores the default keyword inside the json schema.
         * If is true, validator applies default values when it's needed
         *
         * @param useDefaults
         *         if true, validator doesn't ignore default values
         * @return {@code this}
         */
        public SchemaLoaderBuilder useDefaults(boolean useDefaults) {
            this.useDefaults = useDefaults;
            return this;
        }

        public SchemaLoaderBuilder nullableSupport(boolean nullableSupport) {
            this.nullableSupport = nullableSupport;
            return this;
        }

        public SchemaLoaderBuilder regexpFactory(RegexpFactory regexpFactory) {
            this.regexpFactory = regexpFactory;
            return this;
        }

        public SchemaLoaderBuilder registerSchemaByURI(URI uri, Object schema) {
            if (schemasByURI == null) {
                schemasByURI = new HashMap<>();
            }
            schemasByURI.put(uri, schema);
            return this;
        }

        public SchemaLoaderBuilder enableOverrideOfBuiltInFormatValidators() {
            enableOverrideOfBuiltInFormatValidators = true;
            return this;
        }
    }

    public static SchemaLoaderBuilder builder() {
        return new SchemaLoaderBuilder();
    }

    /**
     * Loads a JSON schema to a schema validator using a {@link DefaultSchemaClient default HTTP
     * client}.
     *
     * @param schemaJson
     *         the JSON representation of the schema.
     * @return the schema validator object
     */
    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 schemaClient
     *         the HTTP client to be used for resolving remote JSON references.
     * @return the created schema
     */
    public static Schema load(final JSONObject schemaJson, final SchemaClient schemaClient) {
        SchemaLoader loader = builder()
                .schemaJson(schemaJson)
                .schemaClient(schemaClient)
                .build();
        return loader.load().build();
    }

    private final LoaderConfig config;

    private final LoadingState ls;

    /**
     * Constructor.
     *
     * @param builder
     *         the builder containing the properties. Only {@link SchemaLoaderBuilder#id} is
     *         nullable.
     * @throws NullPointerException
     *         if any of the builder properties except {@link SchemaLoaderBuilder#id id} is
     *         {@code null}.
     */
    public SchemaLoader(SchemaLoaderBuilder builder) {
        Object effectiveRootSchemaJson = builder.rootSchemaJson == null
                ? builder.schemaJson
                : builder.rootSchemaJson;
        Optional schemaKeywordValue = extractSchemaKeywordValue(effectiveRootSchemaJson);
        SpecificationVersion specVersion;
        if (schemaKeywordValue.isPresent()) {
            try {
                specVersion = SpecificationVersion.getByMetaSchemaUrl(schemaKeywordValue.get());
            } catch (IllegalArgumentException e) {
                if (builder.specVersionIsExplicitlySet) {
                    specVersion = builder.specVersion;
                } else {
                    throw new SchemaException("#", "could not determine version");
                }
            }
        } else {
            specVersion = builder.specVersion;
        }
        this.config = new LoaderConfig(builder.schemaClient,
                builder.formatValidators,
                builder.schemasByURI,
                specVersion,
                builder.useDefaults,
                builder.nullableSupport,
                builder.regexpFactory);
        this.ls = new LoadingState(config,
                builder.pointerSchemas,
                effectiveRootSchemaJson,
                builder.schemaJson,
                builder.id,
                builder.pointerToCurrentObj,
                builder.subschemaRegistries);
    }

    private static Optional extractSchemaKeywordValue(Object effectiveRootSchemaJson) {
        if (effectiveRootSchemaJson instanceof Map) {
            Map schemaObj = (Map) effectiveRootSchemaJson;
            Object schemaValue = schemaObj.get("$schema");
            if (schemaValue != null) {
                return Optional.of((String) schemaValue);
            }
        }
        if (effectiveRootSchemaJson instanceof JsonObject) {
            JsonObject schemaObj = (JsonObject) effectiveRootSchemaJson;
            Object schemaValue = schemaObj.get("$schema");
            if (schemaValue != null) {
                return Optional.of((String) schemaValue);
            }
        }
        return Optional.empty();
    }

    SchemaLoader(LoadingState ls) {
        this.ls = ls;
        this.config = ls.config;
    }

    private Schema.Builder loadSchemaBoolean(Boolean rawBoolean) {
        return rawBoolean ? TrueSchema.builder() : FalseSchema.builder();
    }

    private Schema.Builder loadSchemaObject(JsonObject o) {
        AdjacentSchemaExtractionState postExtractionState = runSchemaExtractors(o);
        Collection> extractedSchemas = postExtractionState.extractedSchemaBuilders();
        Schema.Builder effectiveReturnedSchema;
        if (extractedSchemas.isEmpty()) {
            effectiveReturnedSchema = EmptySchema.builder();
        } else if (extractedSchemas.size() == 1) {
            effectiveReturnedSchema = extractedSchemas.iterator().next();
        } else {
            Collection built = extractedSchemas.stream()
                    .map(Schema.Builder::build)
                    .map(Schema.class::cast)
                    .collect(toList());
            effectiveReturnedSchema = CombinedSchema.allOf(built).isSynthetic(true);
        }
        AdjacentSchemaExtractionState postCommonPropLoadingState = loadCommonSchemaProperties(effectiveReturnedSchema, postExtractionState);
        Map unprocessed = postCommonPropLoadingState.projectedSchemaJson().toMap();
        effectiveReturnedSchema.unprocessedProperties(unprocessed);
        return effectiveReturnedSchema;
    }

    private AdjacentSchemaExtractionState runSchemaExtractors(JsonObject o) {
        AdjacentSchemaExtractionState state = new AdjacentSchemaExtractionState(o);
        if (o.containsKey("$ref")) {
            ExtractionResult result = new ReferenceSchemaExtractor(this).extract(o);
            state = state.reduce(result);
            return state;
        }
        List extractors = asList(
                new EnumSchemaExtractor(this),
                new CombinedSchemaLoader(this),
                new NotSchemaExtractor(this),
                new ConstSchemaExtractor(this),
                new TypeBasedSchemaExtractor(this),
                new PropertySnifferSchemaExtractor(this)
        );
        for (SchemaExtractor extractor : extractors) {
            ExtractionResult result = extractor.extract(state.projectedSchemaJson());
            state = state.reduce(result);
        }
        return state;
    }

    private AdjacentSchemaExtractionState loadCommonSchemaProperties(Schema.Builder builder, AdjacentSchemaExtractionState state) {
        KeyConsumer consumedKeys = new KeyConsumer(state.projectedSchemaJson());
        consumedKeys.maybe(config.specVersion.idKeyword()).map(JsonValue::requireString).ifPresent(builder::id);
        consumedKeys.maybe("title").map(JsonValue::requireString).ifPresent(builder::title);
        consumedKeys.maybe("description").map(JsonValue::requireString).ifPresent(builder::description);
        if (ls.specVersion() == DRAFT_7) {
            consumedKeys.maybe("readOnly").map(JsonValue::requireBoolean).ifPresent(builder::readOnly);
            consumedKeys.maybe("writeOnly").map(JsonValue::requireBoolean).ifPresent(builder::writeOnly);
        }
        if (config.nullableSupport) {
            builder.nullable(consumedKeys.maybe("nullable")
                    .map(JsonValue::requireBoolean)
                    .orElse(Boolean.FALSE));
        }
        if (config.useDefaults) {
            consumedKeys.maybe("default").map(JsonValue::deepToOrgJson).ifPresent(builder::defaultValue);
        }
        builder.schemaLocation(ls.pointerToCurrentObj);
        return state.reduce(new ExtractionResult(consumedKeys.collect(), emptyList()));
    }

    /**
     * Populates a {@code Schema.Builder} instance from the {@code schemaJson} schema definition.
     *
     * @return the builder which already contains the validation criteria of the schema, therefore
     * {@link Schema.Builder#build()} can be immediately used to acquire the {@link Schema}
     * instance to be used for validation
     */
    public Schema.Builder load() {
        return ls.schemaJson
                .canBeMappedTo(Boolean.class, this::loadSchemaBoolean)
                .orMappedTo(JsonObject.class, this::loadSchemaObject)
                .requireAny();
    }

    Schema.Builder loadChild(JsonValue childJson) {
        return new SchemaLoader(childJson.ls).load();
    }

    SpecificationVersion specVersion() {
        return ls.specVersion();
    }

    /**
     * @param formatName
     * @return
     * @deprecated
     */
    @Deprecated
    Optional getFormatValidator(String formatName) {
        return Optional.ofNullable(config.formatValidators.get(formatName));
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy