/*******************************************************************************
* Copyright 2012 Pearson Education
*
* 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.semantictools.jsonld.impl;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.semantictools.jsonld.AmbiguousRestrictionException;
import org.semantictools.jsonld.LdClass;
import org.semantictools.jsonld.LdContainer;
import org.semantictools.jsonld.LdContext;
import org.semantictools.jsonld.LdDatatype;
import org.semantictools.jsonld.LdField;
import org.semantictools.jsonld.LdLiteral;
import org.semantictools.jsonld.LdNode;
import org.semantictools.jsonld.LdObject;
import org.semantictools.jsonld.LdProperty;
import org.semantictools.jsonld.LdQualifiedRestriction;
import org.semantictools.jsonld.LdRestriction;
import org.semantictools.jsonld.LdTerm;
import org.semantictools.jsonld.LdType;
import org.semantictools.jsonld.LdValidationMessage;
import org.semantictools.jsonld.LdValidationReport;
import org.semantictools.jsonld.LdValidationResult;
import org.semantictools.jsonld.LdValidationService;
public class LdValidationServiceImpl implements LdValidationService {
private Set<String> ignoredProperties;
public LdValidationServiceImpl() {
}
@Override
public LdValidationReport validate(LdNode node) {
// To ensure that this method is threadsafe, use a delegate.
Delegate delegate = new Delegate(ignoredProperties);
return delegate.validate(node);
}
/**
* A non-threadsafe implementation.
*
* @author Greg McFall
*
*/
static class Delegate implements LdValidationService {
private Set<String> ignoredProperties;
private LdValidationReport report;
public Delegate(Set<String> ignoredProperties) {
this.ignoredProperties = ignoredProperties;
}
@Override
public LdValidationReport validate(LdNode node) {
report = new LdValidationReport();
if (node.isObject()) {
validateObject("", node.asObject());
}
return report;
}
private void validateObject(String path, LdObject obj) {
String objectType = obj.getTypeIRI();
if (objectType == null) {
objectType = inferQualifiedType(path, obj);
obj.setTypeIRI(objectType);
}
RandomAccessObject object = new RandomAccessObject(obj);
LdContext context = obj.getContext();
// TODO: Do we need to check restrictions on superclasses in the type hierarchy?
if (path.length()>0) {
path = path + ".";
}
LdClass dr = null;
if (context == null) {
warn(path, "JSON-LD context is undefined");
} else {
dr = context.getClass(objectType);
}
if (dr != null) {
validateObject(path, object, dr);
}
validateFields(path, object);
}
private String inferQualifiedType(String path, LdObject obj) {
LdField field = obj.owner();
if (field == null) return null;
LdObject ownerObject = field.getOwner();
String ownerType = ownerObject.getTypeIRI();
if (ownerType == null) return null;
LdContext context = obj.getContext();
LdTerm term = context.getTerm(field.getLocalName());
if (term == null) return null;
LdProperty property = term.getProperty();
if (property == null) return null;
LdClass ownerClass = context.getClass(ownerType);
if (ownerClass == null) return null;
String result = null;
try {
result = ownerClass.inferQualifiedPropertyType(field.getPropertyURI());
} catch (AmbiguousRestrictionException e) {
// TODO: construct better error message.
error(path, "The type of this property is ambiguous");
}
return result;
}
private void error(String path, String message) {
report(LdValidationResult.ERROR, path, message);
}
private void warn(String path, String message) {
report(LdValidationResult.WARNING, path, message);
}
private void validateFields(String path, RandomAccessObject obj) {
Iterator<LdField> sequence = obj.getNode().fields();
if (sequence == null) return;
while (sequence.hasNext()) {
LdField field = sequence.next();
String fieldName = field.getLocalName();
String fieldPath = path + fieldName;
validateField(fieldPath, field);
}
}
private void validateObject(String path, RandomAccessObject object, LdClass dr) {
if (dr.listRestrictions()==null) return;
for (LdRestriction restriction : dr.listRestrictions()) {
String propertyURI = restriction.getPropertyURI();
LdField field = object.getField(propertyURI);
String fieldName = getFieldName(object, field, propertyURI);
String fieldPath = path + fieldName;
Integer minCardinality = restriction.getMinCardinality();
Integer maxCardinality = restriction.getMaxCardinality();
LdContext context = object.getNode().getContext();
int cardinality = getCardinality(field);
if (minCardinality != null) {
validateMinCardinality(context, object.getNode(), propertyURI, fieldPath, minCardinality, cardinality);
}
if (maxCardinality != null) {
validateMaxCardinality(fieldPath, maxCardinality, cardinality);
}
validateQualifiedRestrictions(context, fieldPath, restriction, field, cardinality);
}
validateSuperDomains(path, object, dr);
}
private void validateQualifiedRestrictions(
LdContext context,
String fieldPath, LdRestriction restriction, LdField field, int cardinality) {
List<LdQualifiedRestriction> list = restriction.listQualifiedRestrictions();
if (list == null) return;
for (LdQualifiedRestriction qr : list) {
Integer minCardinality = qr.getMinCardinality();
Integer maxCardinality = qr.getMaxCardinality();
LdObject owner = field==null ? null : field.getOwner();
String propertyURI = field==null ? qr.getRestriction().getPropertyURI() : field.getPropertyURI();
LdContext c = (owner==null) ? context : owner.getContext();
if (minCardinality != null) {
validateMinCardinality(c, owner, propertyURI, fieldPath, minCardinality, cardinality);
}
if (maxCardinality != null) {
validateMaxCardinality(fieldPath, maxCardinality, cardinality);
}
}
}
private void validateSuperDomains(String path, RandomAccessObject object, LdClass dr) {
List<LdClass> superList = dr.listSupertypes();
if (superList == null) return;
for (LdClass superDomain : superList) {
validateObject(path, object, superDomain);
}
}
private void validateField(String path, LdField field) {
if (field == null) return;
if (ignoredProperties!=null && ignoredProperties.contains(field.getPropertyURI())) {
return;
}
LdNode value = field.getValue();
validateDomain(path, field);
if (value.isObject()) {
LdTerm term = field.getOwner().getContext().getTerm(field.getPropertyURI());
if (term != null && !"@id".equals(term.getRawTypeIRI())) {
validateObject(path, value.asObject());
}
} else if (value.isContainer()) {
validateContainer(path, value.asContainer());
} else if (value.isLiteral()) {
validateLiteral(path, field, value.asLiteral());
}
}
private void validateLiteral(String path, LdField field, LdLiteral value) {
// TODO: need to handle qualified restrictions.
LdObject owner = field.getOwner();
LdContext context = (owner==null) ? null : owner.getContext();
if (context == null) {
String msg =
"Cannot validate this property because the JSON-LD context is not defined.";
warn(path, msg);
return;
}
LdTerm fieldTerm = context.getTerm(field.getLocalName());
if (fieldTerm == null) {
String msg = "No term is defined for this property";
error(path, msg);
return;
}
String typeIRI = fieldTerm.getTypeIRI();
if (typeIRI == null) {
String msg = "Cannot validate this property because the datatype is not known.";
warn(path, msg);
return;
}
LdDatatype datatype = context.findDatatypeByURI(typeIRI);
if (datatype == null) {
// Don't warn about rdfs:label
if (!"http://www.w3.org/2000/01/rdf-schema#label".equals(fieldTerm.getIRI())) {
// It is possible that the field is supposed to be an embedded object
// but was given an IRI reference as a string value instead.
// Let's test that hypothesis.
LdTerm typeTerm = context.getTerm(typeIRI);
if (typeTerm == null) {
error(path, "Term not found: " + typeIRI);
return;
}
LdClass rdfClass = typeTerm.getRdfClass();
if ("http://www.w3.org/2002/07/owl#Thing".equals(typeIRI)) {
// Special handling for properties of type owl:Thing.
return;
}
if (rdfClass != null) {
String msg = "Expected an embedded object but found an IRI reference";
warn(path, msg);
} else {
String msg = "Cannot validate this property because the datatype is not known.";
warn(path, msg);
}
}
return;
}
validateLiteral(path, value, datatype);
}
private void validateLiteral(String path, LdLiteral value, LdDatatype datatype) {
// TODO: validate dateTime and duration syntax
// String validation
if (
(datatype.getMaxLength() != null) &&
(value.getStringValue().length()>datatype.getMaxLength())
) {
String msg =
"Should have maxLength=" + datatype.getMaxLength() + ", but found length=" +
value.getStringValue().length();
error(path, msg);
}
if (
(datatype.getPattern() != null) &&
(!datatype.getPattern().matcher(value.getStringValue()).matches())
) {
String msg =
"Value does not match the " + datatype.getLocalName() + " pattern: " + datatype.getPattern().pattern();
error(path, msg);
}
// TODO: perform other string validation
}
private void validateDomain(String path, LdField field) {
LdObject owner = field.getOwner();
LdContext context = owner.getContext();
String fieldName = field.getLocalName();
if ("@graph".equals(fieldName)) {
return;
}
if (context == null) {
warn(path, "JSON-LD context is not defined");
return;
}
LdTerm term = context.getTerm(fieldName);
if (term == null) {
String msg =
"Cannot expand property name to a URI. The term '" + fieldName + "' is not defined.";
report(LdValidationResult.ERROR, path, msg);
return;
}
LdProperty property = term.getProperty();
if (property == null) return;
List<String> domainList = property.getDomain();
if (domainList == null) {
if (!"http://www.w3.org/2000/01/rdf-schema#label".equals(term.getIRI())) {
// Special handling for rdfs:label
String msg =
"The domain for the property '" + fieldName + "' is not known.";
report(LdValidationResult.WARNING, path, msg);
}
return;
}
String ownerType = getOwnerType(field);
if (ownerType == null) {
String msg =
"Cannot evaluate the domain of this property because the type of the parent object is not known.";
report(LdValidationResult.WARNING, path, msg);
return;
}
if (!isMemberOf(context, ownerType, domainList)) {
String msg = "Invalid domain for this property";
report(LdValidationResult.ERROR, path, msg);
}
}
private String getOwnerType(LdField field) {
LdObject parent = field.getOwner();
String parentType = parent.getTypeIRI();
if (parentType == null) {
LdField parentField = parent.owner();
if (parentField != null) {
String parentPropertyURI = parentField.getPropertyURI();
LdObject grandParent = parentField.getOwner();
String grandParentType = grandParent.getTypeIRI();
if (grandParentType != null) {
LdTerm term = grandParent.getContext().getTerm(grandParentType);
if (term != null) {
LdClass grandParentClass = term.getRdfClass();
if (grandParentClass != null) {
List<LdRestriction> restrictions = grandParentClass.listRestrictions();
if (restrictions != null) {
for (LdRestriction r : restrictions) {
if (parentPropertyURI.equals(r.getPropertyURI())) {
String parentLdType = r.getAllValuesFrom();
if (parentLdType != null) {
parent.setTypeIRI(parentLdType);
return parentLdType;
}
}
}
}
}
}
}
}
}
return parentType;
}
/**
* Returns true if the type named by the typeURI parameter is one of the
* classes in the given uriList, or a subclass of one of those classes.
*/
private boolean isMemberOf(
LdContext context, String typeURI, List<String> uriList
) {
if (uriList.contains(typeURI)) return true;
LdClass dr = context.getClass(typeURI);
return isSuperMemberOf(context, uriList, dr);
}
private boolean isSuperMemberOf(LdContext context, List<String> uriList, LdClass dr) {
if (dr == null) return false;
List<LdClass> list = dr.listSupertypes();
if (list != null) {
for (LdClass superDR : list) {
String domainURI = superDR.getURI();
if (uriList.contains(domainURI)) return true;
if (isSuperMemberOf(context, uriList, superDR)) return true;
}
}
return false;
}
private void validateContainer(String path, LdContainer container) {
int index = 0;
Iterator<LdNode> sequence = container.iterator();
LdField field = container.owner();
LdObject ownerObject = field.getOwner();
LdContext context = ownerObject.getContext();
boolean idref = false;
LdTerm term = context.getTerm(field.getPropertyURI());
if (term != null && "@id".equals(term.getRawTypeIRI())) {
idref = true;
}
while (sequence.hasNext()) {
LdNode node = sequence.next();
String elemPath = path + "[" + index + "]";
if (node.isObject()) {
validateObject(elemPath, node.asObject());
} if (node.isLiteral()) {
if (idref && !node.asLiteral().isStringValue()) {
warn(elemPath, "Expected an IRI reference");
} else {
validateLiteral(elemPath, field, node.asLiteral());
}
}
index++;
}
}
private void validateMaxCardinality(
String fieldPath, int maxCardinality, int cardinality) {
if (cardinality > maxCardinality) {
String message =
"Expected maxCardinality=" + maxCardinality + ", but found cardinality=" + cardinality;
report(LdValidationResult.ERROR, fieldPath, message);
}
}
private String getFieldName(RandomAccessObject object, LdField field, String propertyURI) {
if (field != null) {
return field.getLocalName();
}
LdContext context = object.getNode().getContext();
if (context == null) {
return getLocalName(propertyURI);
}
LdTerm term = context.getTerm(propertyURI);
return term==null ? getLocalName(propertyURI) : term.getShortName();
}
private String getLocalName(String propertyURI) {
int delim = propertyURI.lastIndexOf('#');
if (delim < 0) {
delim = propertyURI.lastIndexOf('/');
}
if (delim < 0) {
return propertyURI;
}
return propertyURI.substring(delim+1);
}
private void validateMinCardinality(
LdContext context,
LdObject object, String propertyURI, String fieldPath, int minCardinality, int cardinality) {
/**
* Check to see if the minimum cardinality has an override in the
* JSON-LD context.
*/
LdTerm term = context.getTerm(propertyURI);
if (term.getMinCardinality() != null) {
minCardinality = term.getMinCardinality();
}
if (cardinality < minCardinality) {
String message =
"Expected minCardinality=" + minCardinality + ", but found cardinality=" + cardinality;
report(LdValidationResult.ERROR, fieldPath, message);
}
}
private void report(LdValidationResult result, String path, String message) {
report.add(new LdValidationMessage(result, path, message));
}
private int getCardinality(LdField field) {
if (field == null) return 0;
LdNode node = field.getValue();
if (!node.isContainer()) return 1;
return node.asContainer().size();
}
static class RandomAccessObject {
private Map<String, LdField> fieldMap;
private LdObject object;
public RandomAccessObject(LdObject object) {
this.object = object;
fieldMap = new HashMap<String, LdField>();
Iterator<LdField> sequence = object.fields();
if (sequence != null) {
while (sequence.hasNext()) {
LdField field = sequence.next();
fieldMap.put(field.getPropertyURI(), field);
}
}
}
public LdObject getNode() {
return object;
}
public LdField getField(String propertyURI) {
return fieldMap.get(propertyURI);
}
public String toString() {
return "RadnomAccessObject(" + object.getId() + ")";
}
}
@Override
public void setIgnoredProperties(Set<String> propertySet) {
this.ignoredProperties = propertySet;
}
@Override
public Set<String> getIgnoredProperties() {
return ignoredProperties;
}
}
@Override
public void setIgnoredProperties(Set<String> propertySet) {
this.ignoredProperties = propertySet;
}
@Override
public Set<String> getIgnoredProperties() {
return ignoredProperties;
}
}