org.everit.json.schema.ObjectSchema.java Source code

Java tutorial

Introduction

Here is the source code for org.everit.json.schema.ObjectSchema.java

Source

/*
 * 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.json.JSONObject;

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

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

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

        private boolean requiresObject = true;

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

        private boolean additionalProperties = true;

        private Schema schemaOfAdditionalProperties;

        private final List<String> requiredProperties = new ArrayList<String>(0);

        private Integer minProperties;

        private Integer maxProperties;

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

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

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

        public Builder additionalProperties(final boolean additionalProperties) {
            this.additionalProperties = additionalProperties;
            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 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) {
            Objects.requireNonNull(propName, "propName cannot be null");
            Objects.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;
        }

        /**
         * 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<String> dependencies = propertyDependencies.get(ifPresent);
            if (dependencies == null) {
                dependencies = new HashSet<String>(1);
                propertyDependencies.put(ifPresent, dependencies);
            }
            dependencies.add(mustBePresent);
            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 final Map<String, Schema> propertySchemas;

    private final boolean additionalProperties;

    private final Schema schemaOfAdditionalProperties;

    private final List<String> requiredProperties;

    private final Integer minProperties;

    private final Integer maxProperties;

    private final Map<String, Set<String>> propertyDependencies;

    private final Map<String, Schema> schemaDependencies;

    private final boolean requiresObject;

    private final Map<Pattern, Schema> 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 = Collections.unmodifiableMap(builder.propertyDependencies);
        this.schemaDependencies = Collections.unmodifiableMap(builder.schemaDependencies);
        this.requiresObject = builder.requiresObject;
        this.patternProperties = Collections.unmodifiableMap(builder.patternProperties);
    }

    private void failure(final String exceptionMessage, final Object... params) {
        throw new ValidationException(String.format(exceptionMessage, params));
    }

    public Integer getMaxProperties() {
        return maxProperties;
    }

    public Integer getMinProperties() {
        return minProperties;
    }

    public Map<String, Set<String>> getPropertyDependencies() {
        return propertyDependencies;
    }

    public Map<String, Schema> getPropertySchemas() {
        return propertySchemas;
    }

    public List<String> getRequiredProperties() {
        return requiredProperties;
    }

    public Map<String, Schema> getSchemaDependencies() {
        return schemaDependencies;
    }

    public Schema getSchemaOfAdditionalProperties() {
        return schemaOfAdditionalProperties;
    }

    public boolean permitsAdditionalProperties() {
        return additionalProperties;
    }

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

    private void testAdditionalProperties(final JSONObject subject) {
        if (!additionalProperties) {
            getAdditionalProperties(subject).findFirst()
                    .ifPresent(unneeded -> failure("extraneous key [%s] is not permitted", unneeded));
        } else if (schemaOfAdditionalProperties != null) {
            getAdditionalProperties(subject).map(subject::get).forEach(schemaOfAdditionalProperties::validate);
        }
    }

    private Stream<String> 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));
        }
    }

    private void testProperties(final JSONObject subject) {
        if (propertySchemas != null) {
            for (Entry<String, Schema> entry : propertySchemas.entrySet()) {
                String key = entry.getKey();
                if (subject.has(key)) {
                    entry.getValue().validate(subject.get(key));
                }
            }
        }
    }

    private void testPropertyDependencies(final JSONObject subject) {
        propertyDependencies.keySet().stream().filter(subject::has)
                .flatMap(ifPresent -> propertyDependencies.get(ifPresent).stream())
                .filter(mustBePresent -> !subject.has(mustBePresent)).findFirst()
                .ifPresent(missing -> failure("property [%s] is required", missing));
    }

    private void testRequiredProperties(final JSONObject subject) {
        requiredProperties.stream().filter(key -> !subject.has(key)).findFirst()
                .ifPresent(missing -> failure("required key [%s] not found", missing));
    }

    private void testSchemaDependencies(final JSONObject subject) {
        schemaDependencies.keySet().stream().filter(subject::has).map(schemaDependencies::get)
                .forEach(schema -> schema.validate(subject));
    }

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

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

    private void testPatternProperties(final JSONObject subject) {
        String[] propNames = JSONObject.getNames(subject);
        if (propNames == null || propNames.length == 0) {
            return;
        }
        for (Entry<Pattern, Schema> entry : patternProperties.entrySet()) {
            for (String propName : propNames) {
                if (entry.getKey().matcher(propName).find()) {
                    entry.getValue().validate(subject.get(propName));
                }
            }
        }
    }

    public boolean requiresObject() {
        return requiresObject;
    }

    public Map<Pattern, Schema> getPatternProperties() {
        return patternProperties;
    }

}