
java.fedora.server.validation.RelsExtValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fcrepo-client Show documentation
Show all versions of fcrepo-client Show documentation
The Fedora Client is a Java Library that allows API access to a Fedora Repository. The client is typically one part of a full Fedora installation.
The newest version!
/*
* -----------------------------------------------------------------------------
*
* License and Copyright: The contents of this file are subject to 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.fedora-commons.org/licenses.
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
* the specific language governing rights and limitations under the License.
*
* The entire file consists of original code.
* Copyright © 2008 Fedora Commons, Inc.
*
Copyright © 2002-2007 The Rector and Visitors of the University of
* Virginia and Cornell University
* All rights reserved.
*
* -----------------------------------------------------------------------------
*/
package fedora.server.validation;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import fedora.common.Constants;
import fedora.common.PID;
import fedora.server.errors.RepositoryConfigurationException;
import fedora.server.errors.StreamIOException;
import fedora.server.errors.ValidationException;
/**
* This class will validate relationship metadata that may exist in a digital
* object. The validator will SAX parse the content of the RELS-EXT datastream
* which must be an RDF stream that asserts relationships for a digital object.
* The validator will enforce the following restrictions on the RDF stream:
*
* 1. The RDF must follow a prescribed RDF/XML authoring style where there is
* ONE subject encoded as an RDF with an RDF 'about' attribute
* containing a digital object URI. The sub-elements are the relationship
* properties of the subject. Each relationship may refer to any resource
* (identified by URI) via an RDF 'resource' attribute, or a literal.
* Relationship assertions can be from the default Fedora relationship ontology,
* or from other namespaces. For example:
* Bob Smith
*
* 2. There must be only ONE RDF in the RELS-EXT datastream.
*
* 3. There must be NO nesting of assertions. In terms of XML depth, the RDF
* root element is considered depth of 0. Then, the RDF must be at
* depth of 1, and the relationship properties must exist at depth of 2. That's
* it.
*
* 4. The RDF 'about' attribute of the RDF must be the URI of the
* digital object in which the RELS-EXT datastream resides. This means that all
* relationships are FROM "this" object to other objects.
*
* 5. If the target of the statement is a resource (identified by a URI), the
* RDF 'resource' attribute must specify a syntactically valid, absolute URI.
*
* 6. The RDF 'resource' attribute of a relationship assertion must NOT be the
* URI of the digital object that is the subject of the relationships. In other
* words, NO SELF-REFERENTIAL relationships.
*
* 7. There must NOT be any assertion of properties from the DC namespace or
* from the Fedora object properties namespaces (model and view). This is
* because these assertions exist elsewhere in a Fedora digital object and we do
* not want duplication. The RELS-EXT datasream is reserved for relationship
* metadata.
*
*
* @author [email protected]
* @version $Id: RelsExtValidator.java 6425 2007-12-17 17:17:27Z pangloss $
*/
public class RelsExtValidator extends DefaultHandler {
/** Logger for this class. */
private static final Logger LOG = Logger.getLogger(RelsExtValidator.class
.getName());
// Namespace URIs
private final static String OAIDC = "http://www.openarchives.org/OAI/2.0/oai_dc/";
private final static String FMODEL = fedora.common.Constants.MODEL.uri;
private final static String FVIEW = fedora.common.Constants.VIEW.uri;
private final static String RDF = fedora.common.Constants.RDF.uri;
private final static String DC = fedora.common.Constants.DC.uri;
// state variables
private String m_characterEncoding;
private String m_doURI;
private boolean m_rootRDFFound;
private boolean m_descriptionFound;
private int m_depth;
private String m_literalType;
private StringBuffer m_literalValue;
// SAX parser
private SAXParser m_parser;
public RelsExtValidator(String characterEncoding, boolean validate)
throws ParserConfigurationException, SAXException,
UnsupportedEncodingException {
m_characterEncoding = characterEncoding;
new String("test").getBytes(m_characterEncoding);
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setValidating(validate);
spf.setNamespaceAware(true);
m_parser = spf.newSAXParser();
}
public static RelsExtValidator getInstance()
throws RepositoryConfigurationException {
try {
return new RelsExtValidator("UTF-8", false);
} catch (Exception e) {
throw new RepositoryConfigurationException("RelsExtValidator:"
+ "Error instantiating RELS-EXT datastream validator:"
+ e.getClass().getName() + " : " + e.getMessage());
}
}
public void deserialize(InputStream relsDS, String doURI)
throws StreamIOException, SAXException {
LOG.debug("Deserializing RELS-EXT...");
m_rootRDFFound = false;
m_descriptionFound = false;
m_depth = 0;
m_doURI = doURI;
try {
m_parser.parse(relsDS, this);
} catch (IOException ioe) {
throw new StreamIOException("RelsExtValidator:"
+ " low-level stream IO problem occurred"
+ " while SAX parsing RELS-EXT datastream.");
} catch (SAXException se) {
throw new SAXException(se.getMessage());
}
LOG.debug("Just finished parse.");
}
public void startElement(String nsURI, String localName, String qName,
Attributes a) throws SAXException {
if (nsURI.equals(RDF) && localName.equalsIgnoreCase("RDF")) {
m_rootRDFFound = true;
} else if (m_rootRDFFound) {
if (nsURI.equals(RDF) && localName.equalsIgnoreCase("Description")) {
if (!m_descriptionFound) {
m_descriptionFound = true;
m_depth++;
checkDepth(m_depth, qName);
checkAboutURI(grab(a, RDF, "about"));
} else {
throw new SAXException("RelsExtValidator:"
+ " Only ONE RDF element is allowed"
+ " in the RELS-EXT datastream.");
}
} else if (m_descriptionFound) {
m_depth++;
checkDepth(m_depth, qName);
checkBadAssertion(nsURI, localName, qName);
String resourceURI = grab(a, RDF, "resource");
if (resourceURI.length() > 0) {
checkResourceURI(resourceURI, qName);
m_literalType = null;
m_literalValue = null;
} else {
String datatypeURI = grab(a, RDF, "datatype");
if (datatypeURI.length() == 0) {
m_literalType = null;
} else {
m_literalType = datatypeURI;
}
m_literalValue = new StringBuffer();
}
} else {
throw new SAXException("RelsExtValidator:"
+ " Invalid element " + localName
+ " found in the RELS-EXT datastream.\n"
+ " Relationship assertions must be built"
+ " upon an RDF element.");
}
} else {
throw new SAXException("RelsExtValidator:"
+ " The 'RDF' root element was not found "
+ " in the RELS-EXT datastream.\n"
+ " Relationship metadata must be encoded using RDF/XML.");
}
}
public void characters(char[] ch, int start, int length) {
if (m_literalValue != null) {
m_literalValue.append(ch, start, length);
}
}
public void endElement(String nsURI, String localName, String qName)
throws SAXException {
if (m_rootRDFFound && m_descriptionFound) {
m_depth--;
}
if (m_literalType != null && m_literalValue != null) {
checkTypedValue(m_literalType, m_literalValue.toString(), qName);
}
m_literalType = null;
m_literalValue = null;
}
public static void validate(PID pid, InputStream datastream)
throws ValidationException {
try {
RelsExtValidator validator = RelsExtValidator.getInstance();
validator.deserialize(datastream, pid.toURI());
} catch (Exception e) {
throw new ValidationException(e.getMessage(), e);
}
}
private static String grab(Attributes a, String namespace,
String elementName) {
String ret = a.getValue(namespace, elementName);
if (ret == null) {
ret = a.getValue(elementName);
}
// set null attribute value to empty string since it's
// generally helpful in the code to avoid null pointer exception
// when operations are performed on attributes values.
if (ret == null) {
ret = "";
}
return ret;
}
/**
* checkDepth: checks that there is NO nesting of relationship assertions.
* In terms of XML depth, the RDF root element is considered depth of 0.
* Then, the RDF must be at depth of 1, and the relationship
* properties must exist at depth of 2. That's it.
*
* @param depth -
* the depth of the XML element being evaluated
* @param qName -
* the name of the relationship property being evaluated
* @throws SAXException
*/
private void checkDepth(int depth, String qName) throws SAXException {
if (depth > 2) {
throw new SAXException("RelsExtValidator:"
+ " The RELS-EXT datastream has improper"
+ " nesting in its relationship assertions.\n"
+ " (The XML depth is " + depth
+ " which must not exceed a depth of 2.\n"
+ " The root element should be level 0,"
+ " the element should be level 1,"
+ " and relationship elements should be level 2.)");
}
}
/**
* checkBadAssertion: checks that there are NOT be any assertions of
* properties from the DC or Fedora properties namespaces. This is because
* these assertions exist elsewhere in a Fedora digital object and we do not
* want duplication.
*
* @param nsURI -
* the namespace URI of the property being evaluated
* @param localName -
* the local name of the property being evaluated
* @param qName -
* the qualified name of the property being evaluated
* @throws SAXException
*/
private void checkBadAssertion(String nsURI, String localName, String qName)
throws SAXException {
if (nsURI.equals(DC) || nsURI.equals(OAIDC)) {
throw new SAXException("RelsExtValidator:"
+ " The RELS-EXT datastream has improper"
+ " relationship assertion: " + qName + ".\n"
+ " No Dublin Core assertions allowed"
+ " in Fedora relationship metadata.");
} else if (nsURI.equals(FMODEL) || nsURI.equals(FVIEW)) {
throw new SAXException("RelsExtValidator:"
+ " The RELS-EXT datastream has improper"
+ " relationship assertion: " + qName + ".\n"
+ " Relationship metadata cannot contain"
+ " assertions from the Fedora object properties"
+ " namespaces.");
}
}
/**
* checkAboutURI: ensure that the RDF is about the digital
* object that contains the RELS-EXT datastream, since the REL-EXT
* datastream is only supposed to capture relationships about "this" digital
* object.
*
* @param aboutURI -
* the URI value of the RDF 'about' attribute
* @throws SAXException
*/
private void checkAboutURI(String aboutURI) throws SAXException {
if (!m_doURI.equals(aboutURI)) {
throw new SAXException("RelsExtValidator:"
+ " The RELS-EXT datastream refers to"
+ " an improper URI in the 'about' attribute of the"
+ " RDF element.\n"
+ " The URI must be that of the digital object"
+ " in which the RELS-EXT datastream resides" + " ("
+ m_doURI + ").");
}
}
/**
* checkResourceURI: ensure that the target resource is a proper URI and is
* not self-referential.
*
* @param resourceURI -
* the URI value of the RDF 'resource' attribute
* @param relName -
* the name of the relationship property being evaluated
* @throws SAXException
*/
private void checkResourceURI(String resourceURI, String relName)
throws SAXException {
URI uri;
try {
uri = new URI(resourceURI);
} catch (Exception e) {
throw new SAXException("RelsExtValidator:"
+ "Error in relationship '" + relName + "'."
+ " The RDF 'resource' is not a valid URI.");
}
if (!uri.isAbsolute()) {
throw new SAXException("RelsExtValidator:"
+ "Error in relationship '" + relName + "'."
+ " The specified RDF 'resource' is not an absolute URI.");
}
if (resourceURI.equals(m_doURI)) {
throw new SAXException("RelsExtValidator:"
+ " Error in relationship '" + relName + "'."
+ " The RELS-EXT datastream asserts a self-referential"
+ " relationship.\n"
+ " The RDF 'resource' attribute cannot contain the"
+ " URI of the digital object that the relationships"
+ " are about.\n"
+ " Relationships within one object must point"
+ " to the URIs of OTHER digital objects.");
}
}
/**
* checkTypedValue: ensure that the datatype of a literal is one of the
* supported types and that it's a valid value for that type.
*
* @param datatypeURI -
* the URI value of the RDF 'datatype' attribute
* @param value -
* the value
* @param relName -
* the name of the property being evaluated
* @throws SAXException
*/
private void checkTypedValue(String datatypeURI, String value,
String relName) throws SAXException {
if (datatypeURI.equals(Constants.XSD.INT.uri)) {
try {
Integer.parseInt(value);
} catch (Exception e) {
throw new SAXException("RelsExtValidator:"
+ " The value specified for " + relName
+ " is not a valid 'int' value");
}
} else if (datatypeURI.equals(Constants.XSD.LONG.uri)) {
try {
Long.parseLong(value);
} catch (Exception e) {
throw new SAXException("RelsExtValidator:"
+ " The value specified for " + relName
+ " is not a valid 'long' value");
}
} else if (datatypeURI.equals(Constants.XSD.FLOAT.uri)) {
try {
Float.parseFloat(value);
} catch (Exception e) {
throw new SAXException("RelsExtValidator:"
+ " The value specified for " + relName
+ " is not a valid 'float' value");
}
} else if (datatypeURI.equals(Constants.XSD.DOUBLE.uri)) {
try {
Double.parseDouble(value);
} catch (Exception e) {
throw new SAXException("RelsExtValidator:"
+ " The value specified for " + relName
+ " is not a valid 'double' value");
}
} else if (datatypeURI.equals(Constants.XSD.DATE_TIME.uri)) {
if (!isValidDateTime(value)) {
throw new SAXException("RelsExtValidator:"
+ " The value specified for " + relName
+ " is not a valid 'dateTime' value.\n"
+ "The following dateTime formats are allowed:\n"
+ " yyyy-MM-ddTHH:mm:ss\n"
+ " yyyy-MM-ddTHH:mm:ss.S\n"
+ " yyyy-MM-ddTHH:mm:ss.SS\n"
+ " yyyy-MM-ddTHH:mm:ss.SSS\n"
+ " yyyy-MM-ddTHH:mm:ssZ\n"
+ " yyyy-MM-ddTHH:mm:ss.SZ\n"
+ " yyyy-MM-ddTHH:mm:ss.SSZ\n"
+ " yyyy-MM-ddTHH:mm:ss.SSS\n"
+ " yyyy-MM-ddTHH:mm:ss.SSSZ");
}
} else {
throw new SAXException(
"RelsExtValidator:"
+ " Error in relationship '"
+ relName
+ "'.\n"
+ " The RELS-EXT datastream does not support the specified"
+ " datatype.\n"
+ "If specified, the RDF 'datatype' must be the URI of one of\n"
+ "the following W3C XML Schema data types: int, long, float,\n"
+ "double, or dateTime");
}
}
/**
* Tells whether the given string is a valid lexical representation of a
* dateTime value. Passing this test will ensure successful indexing later.
*/
private static boolean isValidDateTime(String lex) {
SimpleDateFormat format = new SimpleDateFormat();
format.setTimeZone(TimeZone.getTimeZone("UTC"));
int length = lex.length();
if (lex.startsWith("-")) {
length--;
}
if (lex.endsWith("Z")) {
if (length == 20) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
} else if (length == 22) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.S'Z'");
} else if (length == 23) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SS'Z'");
} else if (length == 24) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
} else {
LOG.warn("Not a valid dateTime: " + lex);
return false;
}
} else {
if (length == 19) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss");
} else if (length == 21) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.S");
} else if (length == 22) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SS");
} else if (length == 23) {
format.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
} else {
LOG.warn("Not a valid dateTime: " + lex);
return false;
}
}
try {
format.parse(lex);
if (LOG.isTraceEnabled()) {
LOG.trace("Validated dateTime: " + lex);
}
return true;
} catch (ParseException e) {
LOG.warn("Not a valid dateTime: " + lex);
return false;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy