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

org.everit.json.schema.ObjectSchema Maven / Gradle / Ivy

Go to download

Implementation of the JSON Schema Core Draft v4 specification built with the org.json API

The newest version!
/*
 * Copyright (C) 2011 Everit Kft. (http://www.everit.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.everit.json.schema;

import org.everit.json.schema.internal.JSONPrinter;
import org.json.JSONObject;

import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;

/**
 * Object schema validator.
 */
public class ObjectSchema extends Schema {

    /**
     * Builder class for {@link ObjectSchema}.
     */
    public static class Builder extends Schema.Builder {

        private final Map patternProperties = new HashMap<>();

        private boolean requiresObject = true;

        private final Map propertySchemas = new HashMap<>();

        private boolean additionalProperties = true;

        private Schema schemaOfAdditionalProperties;

        private final List requiredProperties = new ArrayList(0);

        private Integer minProperties;

        private Integer maxProperties;

        private final Map> propertyDependencies = new HashMap<>();

        private final Map schemaDependencies = new HashMap<>();

        public Builder additionalProperties(final boolean additionalProperties) {
            this.additionalProperties = additionalProperties;
            return this;
        }

        /**
         * Adds a property schema.
         *
         * @param propName the name of the property which' expected schema must be {@code schema}
         * @param schema   if the subject under validation has a property named {@code propertyName} then its
         *                 value will be validated using this {@code schema}
         * @return {@code this}
         */
        public Builder addPropertySchema(final String propName, final Schema schema) {
            requireNonNull(propName, "propName cannot be null");
            requireNonNull(schema, "schema cannot be null");
            propertySchemas.put(propName, schema);
            return this;
        }

        public Builder addRequiredProperty(final String propertyName) {
            requiredProperties.add(propertyName);
            return this;
        }

        @Override
        public ObjectSchema build() {
            return new ObjectSchema(this);
        }

        public Builder maxProperties(final Integer maxProperties) {
            this.maxProperties = maxProperties;
            return this;
        }

        public Builder minProperties(final Integer minProperties) {
            this.minProperties = minProperties;
            return this;
        }

        public Builder patternProperty(final Pattern pattern, final Schema schema) {
            this.patternProperties.put(pattern, schema);
            return this;
        }

        public Builder patternProperty(final String pattern, final Schema schema) {
            return patternProperty(Pattern.compile(pattern), schema);
        }

        /**
         * Adds a property dependency.
         *
         * @param ifPresent     the name of the property which if is present then a property with name
         *                      {@code mustBePresent} is mandatory
         * @param mustBePresent a property with this name must exist in the subject under validation if a property
         *                      named {@code ifPresent} exists
         * @return {@code this}
         */
        public Builder propertyDependency(final String ifPresent, final String mustBePresent) {
            Set dependencies = propertyDependencies.get(ifPresent);
            if (dependencies == null) {
                dependencies = new HashSet(1);
                propertyDependencies.put(ifPresent, dependencies);
            }
            dependencies.add(mustBePresent);
            return this;
        }

        public Builder requiresObject(final boolean requiresObject) {
            this.requiresObject = requiresObject;
            return this;
        }

        public Builder schemaDependency(final String ifPresent, final Schema expectedSchema) {
            schemaDependencies.put(ifPresent, expectedSchema);
            return this;
        }

        public Builder schemaOfAdditionalProperties(final Schema schemaOfAdditionalProperties) {
            this.schemaOfAdditionalProperties = schemaOfAdditionalProperties;
            return this;
        }

    }

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

    private static  Map copyMap(final Map original) {
        return Collections.unmodifiableMap(new HashMap<>(original));
    }

    private final Map propertySchemas;

    private final boolean additionalProperties;

    private final Schema schemaOfAdditionalProperties;

    private final List requiredProperties;

    private final Integer minProperties;

    private final Integer maxProperties;

    private final Map> propertyDependencies;

    private final Map schemaDependencies;

    private final boolean requiresObject;

    private final Map patternProperties;

    /**
     * Constructor.
     *
     * @param builder the builder object containing validation criteria
     */
    public ObjectSchema(final Builder builder) {
        super(builder);
        this.propertySchemas = builder.propertySchemas == null ? null
                : Collections.unmodifiableMap(builder.propertySchemas);
        this.additionalProperties = builder.additionalProperties;
        this.schemaOfAdditionalProperties = builder.schemaOfAdditionalProperties;
        if (!additionalProperties && schemaOfAdditionalProperties != null) {
            throw new SchemaException(
                    "additionalProperties cannot be false if schemaOfAdditionalProperties is present");
        }
        this.requiredProperties = Collections.unmodifiableList(new ArrayList<>(
                builder.requiredProperties));
        this.minProperties = builder.minProperties;
        this.maxProperties = builder.maxProperties;
        this.propertyDependencies = copyMap(builder.propertyDependencies);
        this.schemaDependencies = copyMap(builder.schemaDependencies);
        this.requiresObject = builder.requiresObject;
        this.patternProperties = copyMap(builder.patternProperties);
    }

    private Stream getAdditionalProperties(final JSONObject subject) {
        String[] names = JSONObject.getNames(subject);
        if (names == null) {
            return Stream.empty();
        } else {
            return Arrays
                    .stream(names)
                    .filter(key -> !propertySchemas.containsKey(key))
                    .filter(key -> !matchesAnyPattern(key));
        }
    }

    public Integer getMaxProperties() {
        return maxProperties;
    }

    public Integer getMinProperties() {
        return minProperties;
    }

    public Map getPatternProperties() {
        return patternProperties;
    }

    public Map> getPropertyDependencies() {
        return propertyDependencies;
    }

    public Map getPropertySchemas() {
        return propertySchemas;
    }

    public List getRequiredProperties() {
        return requiredProperties;
    }

    public Map getSchemaDependencies() {
        return schemaDependencies;
    }

    public Schema getSchemaOfAdditionalProperties() {
        return schemaOfAdditionalProperties;
    }

    private Optional ifFails(final Schema schema, final Object input) {
        try {
            schema.validate(input);
            return Optional.empty();
        } catch (ValidationException e) {
            return Optional.of(e);
        }
    }

    private boolean matchesAnyPattern(final String key) {
        return patternProperties.keySet().stream()
                .filter(pattern -> pattern.matcher(key).find())
                .findAny()
                .isPresent();
    }

    public boolean permitsAdditionalProperties() {
        return additionalProperties;
    }

    public boolean requiresObject() {
        return requiresObject;
    }

    private List testAdditionalProperties(final JSONObject subject) {
        if (!additionalProperties) {
            return getAdditionalProperties(subject)
                    .map(unneeded -> String.format("extraneous key [%s] is not permitted", unneeded))
                    .map(msg -> new ValidationException(this, msg, "additionalProperties"))
                    .collect(Collectors.toList());
        } else if (schemaOfAdditionalProperties != null) {
            List additionalPropNames = getAdditionalProperties(subject)
                    .collect(Collectors.toList());
            List rval = new ArrayList();
            for (String propName : additionalPropNames) {
                Object propVal = subject.get(propName);
                ifFails(schemaOfAdditionalProperties, propVal)
                        .map(failure -> failure.prepend(propName, this))
                        .ifPresent(rval::add);
            }
            return rval;
        }
        return Collections.emptyList();
    }

    private List testPatternProperties(final JSONObject subject) {
        String[] propNames = JSONObject.getNames(subject);
        if (propNames == null || propNames.length == 0) {
            return Collections.emptyList();
        }
        List rval = new ArrayList<>();
        for (Entry entry : patternProperties.entrySet()) {
            for (String propName : propNames) {
                if (entry.getKey().matcher(propName).find()) {
                    ifFails(entry.getValue(), subject.get(propName))
                            .map(exc -> exc.prepend(propName))
                            .ifPresent(rval::add);
                }
            }
        }
        return rval;
    }

    private List testProperties(final JSONObject subject) {
        if (propertySchemas != null) {
            List rval = new ArrayList<>();
            for (Entry entry : propertySchemas.entrySet()) {
                String key = entry.getKey();
                if (subject.has(key)) {
                    ifFails(entry.getValue(), subject.get(key))
                            .map(exc -> exc.prepend(key))
                            .ifPresent(rval::add);
                }
            }
            return rval;
        }
        return Collections.emptyList();
    }

    private List testPropertyDependencies(final JSONObject subject) {
        return propertyDependencies.keySet().stream()
                .filter(subject::has)
                .flatMap(ifPresent -> propertyDependencies.get(ifPresent).stream())
                .filter(mustBePresent -> !subject.has(mustBePresent))
                .map(missingKey -> String.format("property [%s] is required", missingKey))
                .map(excMessage -> new ValidationException(this, excMessage, "dependencies"))
                .collect(Collectors.toList());
    }

    private List testRequiredProperties(final JSONObject subject) {
        return requiredProperties.stream()
                .filter(key -> !subject.has(key))
                .map(missingKey -> String.format("required key [%s] not found", missingKey))
                .map(excMessage -> new ValidationException(this, excMessage, "required"))
                .collect(Collectors.toList());
    }

    private List testSchemaDependencies(final JSONObject subject) {
        List rval = new ArrayList<>();
        for (Map.Entry schemaDep : schemaDependencies.entrySet()) {
            String propName = schemaDep.getKey();
            if (subject.has(propName)) {
                ifFails(schemaDep.getValue(), subject).ifPresent(rval::add);
            }
        }
        return rval;
    }

    private List testSize(final JSONObject subject) {
        int actualSize = subject.length();
        if (minProperties != null && actualSize < minProperties.intValue()) {
            return Arrays
                    .asList(new ValidationException(this, String.format("minimum size: [%d], found: [%d]",
                            minProperties, actualSize), "minProperties"));
        }
        if (maxProperties != null && actualSize > maxProperties.intValue()) {
            return Arrays
                    .asList(new ValidationException(this, String.format("maximum size: [%d], found: [%d]",
                            maxProperties, actualSize), "maxProperties"));
        }
        return Collections.emptyList();
    }

    @Override
    public void validate(final Object subject) {
        if (!(subject instanceof JSONObject)) {
            if (requiresObject) {
                throw new ValidationException(this, JSONObject.class, subject);
            }
        } else {
            List failures = new ArrayList<>();
            JSONObject objSubject = (JSONObject) subject;
            failures.addAll(testProperties(objSubject));
            failures.addAll(testRequiredProperties(objSubject));
            failures.addAll(testAdditionalProperties(objSubject));
            failures.addAll(testSize(objSubject));
            failures.addAll(testPropertyDependencies(objSubject));
            failures.addAll(testSchemaDependencies(objSubject));
            failures.addAll(testPatternProperties(objSubject));
            ValidationException.throwFor(this, failures);
        }
    }

    @Override
    public boolean definesProperty(String field) {
        field = field.replaceFirst("^#", "").replaceFirst("^/", "");
        int firstSlashIdx = field.indexOf('/');
        String nextToken, remaining;
        if (firstSlashIdx == -1) {
            nextToken = field;
            remaining = null;
        } else {
            nextToken = field.substring(0, firstSlashIdx);
            remaining = field.substring(firstSlashIdx + 1);
        }
        return !field.isEmpty() && (definesSchemaProperty(nextToken, remaining)
                || definesPatternProperty(nextToken, remaining)
                || definesSchemaDependencyProperty(field));
    }

    private boolean definesSchemaProperty(String current, final String remaining) {
        current = unescape(current);
        boolean hasSuffix = !(remaining == null);
        if (propertySchemas.containsKey(current)) {
            if (hasSuffix) {
                return propertySchemas.get(current).definesProperty(remaining);
            } else {
                return true;
            }
        }
        return false;
    }

    private boolean definesPatternProperty(final String current, final String remaining) {
        return patternProperties.keySet()
                .stream()
                .filter(pattern -> pattern.matcher(current).matches())
                .map(pattern -> patternProperties.get(pattern))
                .filter(schema -> remaining == null || schema.definesProperty(remaining))
                .findAny()
                .isPresent();
    }

    private boolean definesSchemaDependencyProperty(final String field) {
        return schemaDependencies.containsKey(field)
                || schemaDependencies.values().stream()
                .filter(schema -> schema.definesProperty(field))
                .findAny()
                .isPresent();
    }

    private String unescape(final String value) {
        return value.replace("~1", "/").replace("~0", "~");
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o instanceof ObjectSchema) {
            ObjectSchema that = (ObjectSchema) o;
            return that.canEqual(this) &&
                    additionalProperties == that.additionalProperties &&
                    requiresObject == that.requiresObject &&
                    Objects.equals(propertySchemas, that.propertySchemas) &&
                    Objects.equals(schemaOfAdditionalProperties, that.schemaOfAdditionalProperties) &&
                    Objects.equals(requiredProperties, that.requiredProperties) &&
                    Objects.equals(minProperties, that.minProperties) &&
                    Objects.equals(maxProperties, that.maxProperties) &&
                    Objects.equals(propertyDependencies, that.propertyDependencies) &&
                    Objects.equals(schemaDependencies, that.schemaDependencies) &&
                    Objects.equals(patternProperties, that.patternProperties) &&
                    super.equals(that);
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), propertySchemas, additionalProperties, schemaOfAdditionalProperties, requiredProperties,
                minProperties, maxProperties, propertyDependencies, schemaDependencies, requiresObject, patternProperties);
    }

    @Override
    void describePropertiesTo(JSONPrinter writer) {
        if (requiresObject) {
            writer.key("type").value("object");
        }
        if (!propertySchemas.isEmpty()) {
            writer.key("properties");
            writer.printSchemaMap(propertySchemas);
        }
        writer.ifPresent("minProperties", minProperties);
        writer.ifPresent("maxProperties", maxProperties);
        if (!requiredProperties.isEmpty()) {
            writer.key("required").value(requiredProperties);
        }
        if (schemaOfAdditionalProperties != null) {
            writer.key("additionalProperties");
            schemaOfAdditionalProperties.describeTo(writer);
        }
        if (!propertyDependencies.isEmpty()) {
            describePropertyDependenciesTo(writer);
        }
        if (!schemaDependencies.isEmpty()) {
            writer.key("dependencies");
            writer.printSchemaMap(schemaDependencies);
        }
        if (!patternProperties.isEmpty()) {
            writer.key("patternProperties");
            writer.printSchemaMap(patternProperties);
        }
        writer.ifFalse("additionalProperties", additionalProperties);
    }

    private void describePropertyDependenciesTo(JSONPrinter writer) {
        writer.key("dependencies");
        writer.object();
        propertyDependencies.entrySet().forEach(entry -> {
            writer.key(entry.getKey());
            writer.array();
            entry.getValue().forEach(writer::value);
            writer.endArray();
        });
        writer.endObject();
    }

    @Override
    protected boolean canEqual(Object other) {
        return other instanceof ObjectSchema;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy