org.everit.json.schema.ObjectSchema Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of json-schema-java7 Show documentation
Show all versions of json-schema-java7 Show documentation
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 com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
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 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 LinkedHashMap<>();
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 LinkedHashSet<>(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 FluentIterable getAdditionalProperties(final JSONObject subject) {
String[] names = JSONObject.getNames(subject);
if (names == null) {
return FluentIterable.from(Lists.newArrayList());
} else {
return FluentIterable.of(names)
.filter(new Predicate() {
@Override
public boolean apply(String key) {
return !propertySchemas.containsKey(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.absent();
} catch (ValidationException e) {
return Optional.of(e);
}
}
private boolean matchesAnyPattern(final String key) {
return FluentIterable.from(patternProperties.keySet())
.firstMatch(new Predicate() {
@Override
public boolean apply(Pattern pattern) {
return pattern.matcher(key).find();
}
})
.isPresent();
}
public boolean permitsAdditionalProperties() {
return additionalProperties;
}
public boolean requiresObject() {
return requiresObject;
}
private ImmutableList testAdditionalProperties(final JSONObject subject) {
if (!additionalProperties) {
return getAdditionalProperties(subject)
.transform(new Function() {
@Override
public ValidationException apply(String unneeded) {
return new ValidationException(ObjectSchema.this,
String.format("extraneous key [%s] is not permitted", unneeded), "additionalProperties");
}
})
.toList();
} else if (schemaOfAdditionalProperties != null) {
List additionalPropNames = getAdditionalProperties(subject).toList();
List rval = new ArrayList();
for (final String propName : additionalPropNames) {
Object propVal = subject.get(propName);
rval.addAll(ifFails(schemaOfAdditionalProperties, propVal)
.transform(new Function() {
@Override
public ValidationException apply(ValidationException failure) {
return failure.prepend(propName, ObjectSchema.this);
}
}).asSet());
}
return ImmutableList.copyOf(rval);
}
return ImmutableList.of();
}
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 (final String propName : propNames) {
if (entry.getKey().matcher(propName).find()) {
rval.addAll(ifFails(entry.getValue(), subject.get(propName))
.transform(new Function() {
@Override
public ValidationException apply(ValidationException exc) {
return exc.prepend(propName);
}
})
.asSet());
}
}
}
return rval;
}
private List testProperties(final JSONObject subject) {
if (propertySchemas != null) {
List rval = new ArrayList<>();
for (Entry entry : propertySchemas.entrySet()) {
final String key = entry.getKey();
if (subject.has(key)) {
rval.addAll(ifFails(entry.getValue(), subject.get(key))
.transform(new Function() {
@Override
public ValidationException apply(ValidationException exc) {
return exc.prepend(key);
}
})
.asSet());
}
}
return rval;
}
return Collections.emptyList();
}
private ImmutableList testPropertyDependencies(final JSONObject subject) {
return FluentIterable.from(propertyDependencies.keySet())
.filter(new Predicate() {
@Override
public boolean apply(String input) {
return subject.has(input);
}
})
.transformAndConcat(new Function>() {
@Override
public Set apply(String input) {
return propertyDependencies.get(input);
}
})
.filter(new Predicate() {
@Override
public boolean apply(String mustBePresent) {
return !subject.has(mustBePresent);
}
})
.transform(new Function() {
@Override
public ValidationException apply(String missingKey) {
return new ValidationException(ObjectSchema.this,
String.format("property [%s] is required", missingKey), "dependencies");
}
})
.toList();
}
private List testRequiredProperties(final JSONObject subject) {
return FluentIterable.from(requiredProperties)
.filter(new Predicate() {
@Override
public boolean apply(String key) {
return !subject.has(key);
}
})
.transform(new Function() {
@Override
public ValidationException apply(String missingKey) {
return new ValidationException(ObjectSchema.this,
String.format("required key [%s] not found", missingKey), "required");
}
})
.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)) {
rval.addAll(ifFails(schemaDep.getValue(), subject).asSet());
}
}
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 FluentIterable.from(patternProperties.keySet())
.filter(new Predicate() {
@Override
public boolean apply(Pattern pattern) {
return pattern.matcher(current).matches();
}
})
.transform(new Function() {
@Override
public Schema apply(Pattern pattern) {
return patternProperties.get(pattern);
}
})
.firstMatch(new Predicate() {
@Override
public boolean apply(Schema schema) {
return remaining == null || schema.definesProperty(remaining);
}
})
.isPresent();
}
private boolean definesSchemaDependencyProperty(final String field) {
return schemaDependencies.containsKey(field)
|| FluentIterable.from(schemaDependencies.values())
.firstMatch(new Predicate() {
@Override
public boolean apply(Schema schema) {
return schema.definesProperty(field);
}
})
.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();
for (Entry> entry : propertyDependencies.entrySet()) {
writer.key(entry.getKey());
writer.array();
for (String value : entry.getValue()) {
writer.value(value);
}
writer.endArray();
}
writer.endObject();
}
@Override
protected boolean canEqual(Object other) {
return other instanceof ObjectSchema;
}
}