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

dev.harrel.jsonschema.JsonParser Maven / Gradle / Ivy

There is a newer version: 1.7.3
Show newest version
package dev.harrel.jsonschema;

import java.net.URI;
import java.util.*;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;

final class JsonParser {
    private final Map dialects;
    private final Dialect defaultDialect;
    private final EvaluatorFactory evaluatorFactory;
    private final SchemaRegistry schemaRegistry;
    private final MetaSchemaValidator metaSchemaValidator;
    private final boolean disabledSchemaValidation;
    private final Map unfinishedSchemas = new HashMap<>();

    JsonParser(Map dialects,
               Dialect defaultDialect,
               EvaluatorFactory evaluatorFactory,
               SchemaRegistry schemaRegistry,
               MetaSchemaValidator metaSchemaValidator,
               boolean disabledSchemaValidation) {
        this.dialects = Objects.requireNonNull(dialects);
        this.defaultDialect = Objects.requireNonNull(defaultDialect);
        this.evaluatorFactory = evaluatorFactory;
        this.schemaRegistry = Objects.requireNonNull(schemaRegistry);
        this.metaSchemaValidator = Objects.requireNonNull(metaSchemaValidator);
        this.disabledSchemaValidation = disabledSchemaValidation;
    }

    synchronized URI parseRootSchema(URI baseUri, JsonNode node) {
        SchemaRegistry.State snapshot = schemaRegistry.createSnapshot();
        try {
            return parseRootSchemaInternal(UriUtil.getUriWithoutFragment(baseUri), node);
        } catch (RuntimeException e) {
            schemaRegistry.restoreSnapshot(snapshot);
            throw e;
        }
    }

    private URI parseRootSchemaInternal(URI baseUri, JsonNode node) {
        Optional> objectMapOptional = JsonNodeUtil.getAsObject(node);
        URI metaSchemaUri = OptionalUtil.firstPresent(
                        () -> objectMapOptional.flatMap(obj -> JsonNodeUtil.getStringField(obj, Keyword.SCHEMA)),
                        () -> Optional.ofNullable(defaultDialect.getMetaSchema())
                )
                .map(UriUtil::removeEmptyFragment)
                .orElse(null);
        Optional idField = objectMapOptional.flatMap(obj -> JsonNodeUtil.getStringField(obj, Keyword.ID));
        Optional providedSchemaId = idField
                .map(UriUtil::getUriWithoutFragment)
                .filter(id -> !baseUri.equals(id));

        UnfinishedSchema unfinishedSchema = new UnfinishedSchema();
        unfinishedSchemas.put(baseUri, unfinishedSchema);
        providedSchemaId.ifPresent(id -> unfinishedSchemas.put(id, unfinishedSchema));

        URI finalUri = providedSchemaId.orElse(baseUri);
        MetaSchemaData metaSchemaData = validateAgainstMetaSchema(node, metaSchemaUri, finalUri.toString());

        if (node.isBoolean()) {
            SchemaParsingContext ctx = new SchemaParsingContext(metaSchemaData, schemaRegistry, baseUri, emptyMap());
            List evaluators = singletonList(new EvaluatorWrapper(null, node, Schema.getBooleanEvaluator(node.asBoolean())));
            schemaRegistry.registerSchema(ctx, node, evaluators);
        } else if (objectMapOptional.isPresent()) {
            Map objectMap = objectMapOptional.get();
            SchemaParsingContext ctx = new SchemaParsingContext(metaSchemaData, schemaRegistry, finalUri, objectMap);
            idField.ifPresent(id -> validateIdField(ctx, id));
            List evaluators = parseEvaluators(ctx, objectMap, node.getJsonPointer());
            schemaRegistry.registerSchema(ctx, node, evaluators);
            providedSchemaId.ifPresent(id -> schemaRegistry.registerAlias(id, baseUri));
        }

        unfinishedSchema.parsed();
        unfinishedSchemas.remove(baseUri);
        providedSchemaId.ifPresent(unfinishedSchemas::remove);

        return finalUri;
    }

    private void parseNode(SchemaParsingContext ctx, JsonNode node) {
        if (node.isBoolean()) {
            parseBoolean(ctx, node);
        } else if (node.isArray()) {
            parseArray(ctx, node);
        } else if (node.isObject()) {
            parseObject(ctx, node);
        }
    }

    private void parseBoolean(SchemaParsingContext ctx, JsonNode node) {
        Evaluator booleanEvaluator = Schema.getBooleanEvaluator(node.asBoolean());
        List evaluators = singletonList(new EvaluatorWrapper(null, node, booleanEvaluator));
        schemaRegistry.registerSchema(ctx, node, evaluators);
    }

    private void parseArray(SchemaParsingContext ctx, JsonNode node) {
        for (JsonNode element : node.asArray()) {
            parseNode(ctx, element);
        }
    }

    private void parseObject(SchemaParsingContext ctx, JsonNode node) {
        Map objectMap = node.asObject();
        Optional idField = JsonNodeUtil.getStringField(objectMap, Keyword.ID);
        boolean isEmbeddedSchema = idField
                .map(id -> !id.startsWith("#") || ctx.getSpecificationVersion().getOrder() > SpecificationVersion.DRAFT7.getOrder())
                .orElse(false);

        if (!isEmbeddedSchema) {
            SchemaParsingContext newCtx = ctx.forChild(objectMap);
            schemaRegistry.registerSchema(ctx, node, parseEvaluators(newCtx, objectMap, node.getJsonPointer()));
        } else {
            /* Embedded schema handling */
            String idString = idField.get();
            URI idUri = URI.create(idString);
            UnfinishedSchema unfinishedSchema = new UnfinishedSchema();
            unfinishedSchemas.put(idUri, unfinishedSchema);
            MetaSchemaData metaSchemaData = JsonNodeUtil.getStringField(objectMap, Keyword.SCHEMA)
                    .map(UriUtil::removeEmptyFragment)
                    .map(metaSchemaUri -> validateAgainstMetaSchema(node, metaSchemaUri, idUri.toString()))
                    .orElse(ctx.getMetaValidationData());

            URI uri = ctx.getParentUri().resolve(idUri);
            SchemaParsingContext newCtx = ctx.forChild(metaSchemaData, objectMap, uri);
            validateIdField(newCtx, idString);
            List evaluators = parseEvaluators(newCtx, objectMap, node.getJsonPointer());
            schemaRegistry.registerEmbeddedSchema(newCtx, uri, node, evaluators);
            unfinishedSchema.parsed();
            unfinishedSchemas.remove(idUri);
        }
    }

    private List parseEvaluators(SchemaParsingContext ctx, Map object, String objectPath) {
        List evaluators = new ArrayList<>();
        JsonNode refOverride = null;
        /* until draft2019, $ref must ignore sibling keywords */
        if (ctx.getSpecificationVersion().getOrder() <= SpecificationVersion.DRAFT7.getOrder()) {
            refOverride = object.get(Keyword.REF);
        }

        for (Map.Entry entry : object.entrySet()) {
            if (refOverride == null || entry.getValue() == refOverride) {
                createEvaluatorFactory(ctx).create(ctx, entry.getKey(), entry.getValue())
                        .map(evaluator -> new EvaluatorWrapper(entry.getKey(), entry.getValue(), evaluator))
                        .ifPresent(evaluators::add);
            }
            parseNode(ctx, entry.getValue());
        }
        if (evaluators.isEmpty()) {
            evaluators.add(new EvaluatorWrapper(null, objectPath, Schema.getBooleanEvaluator(true)));
        }
        return evaluators;
    }

    private MetaSchemaData validateAgainstMetaSchema(JsonNode node, URI metaSchemaUri, String uri) {
        MetaSchemaData data = resolveMetaSchemaData(node, metaSchemaUri, uri);
        new VocabularyValidator().validateVocabularies(data.dialect, data.vocabularyObject);
        return data;
    }

    private MetaSchemaData resolveMetaSchemaData(JsonNode node, URI metaSchemaUri, String uri) {
        if (disabledSchemaValidation) {
            return new MetaSchemaData(dialects.getOrDefault(metaSchemaUri, defaultDialect));
        }

        Dialect dialect = dialects.get(metaSchemaUri);
        UnfinishedSchema unfinishedSchema = unfinishedSchemas.get(metaSchemaUri);
        /* If meta-schema is the same as schema or is currently being processed, its validation needs to be postponed */
        if (unfinishedSchema != null) {
            if (dialect == null) {
                throw MetaSchemaResolvingException.recursiveFailure(metaSchemaUri.toString());
            }
            unfinishedSchema.callbacks.add(() -> metaSchemaValidator.validateSchema(this, metaSchemaUri, uri, node));
            return new MetaSchemaData(dialect);
        }

        MetaSchemaData metaSchemaData =  metaSchemaValidator.validateSchema(this, metaSchemaUri, uri, node);
        if (dialect == null) {
            return metaSchemaData;
        }

        /* If this is a registered dialect and meta-schema defines no vocabs, use vocabs from dialect */
        if (metaSchemaData.vocabularyObject == null) {
            return new MetaSchemaData(dialect, dialect.getDefaultVocabularyObject(), dialect.getDefaultVocabularyObject().keySet());
        } else {
            return new MetaSchemaData(dialect, metaSchemaData.vocabularyObject, metaSchemaData.activeVocabularies);
        }
    }

    private EvaluatorFactory createEvaluatorFactory(SchemaParsingContext ctx) {
        if (evaluatorFactory != null) {
            return EvaluatorFactory.compose(evaluatorFactory, ctx.getMetaValidationData().dialect.getEvaluatorFactory());
        } else {
            return ctx.getMetaValidationData().dialect.getEvaluatorFactory();
        }
    }

    private static void validateIdField(SchemaParsingContext ctx, String id) {
        URI uri = URI.create(id);
        if (ctx.getSpecificationVersion().getOrder() > SpecificationVersion.DRAFT7.getOrder()) {
            if (uri.getFragment() != null && !uri.getFragment().isEmpty()) {
                throw new IllegalArgumentException(String.format("$id [%s] cannot contain non-empty fragments", id));
            }
        } else {
            if (uri.getFragment() != null && uri.getFragment().startsWith("/")) {
                throw new IllegalArgumentException(String.format("$id [%s] cannot contain fragments starting with '/'", id));
            }
        }
    }

    private static final class UnfinishedSchema {
        private final List callbacks = new ArrayList<>();

        void parsed() {
            /* old good for loop to avoid ConcurrentModificationException */
            for (int i = 0; i < callbacks.size(); i++) {
                callbacks.get(i).run();
            }
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy