/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.portal.resource;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.exoplatform.commons.utils.Safe;
import org.exoplatform.commons.xml.DocumentSource;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* A DOM builder and validator for {@code gatein-resources.xml} files.
*
* @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a>
*/
public class GateInResourcesSchemaValidator {
/**
* A simple {@link ErrorHandler} to collect details about issues reported by a SAX parser.
* @see #throwSummary()
*
* @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a>
*/
protected static class ErrorCollector implements ErrorHandler {
private StringBuilder messageBuffer;
private SAXParseException firstException;
private final String resourceId;
/**
* @param resourceId
*/
public ErrorCollector(String resourceId) {
super();
this.resourceId = resourceId;
}
/**
* @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
*/
@Override
public void warning(SAXParseException e) {
log.warn("'"+ resourceId +"' parse warning:", e);
}
/**
* @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
*/
@Override
public void error(SAXParseException e) {
if (firstException == null) {
firstException = e;
messageBuffer = new StringBuilder(64);
messageBuffer.append('\'').append(resourceId).append('\'').append("' validation error:");
}
messageBuffer.append('\n').append(e.toString());
}
/**
* @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
*/
@Override
public void fatalError(SAXParseException e) {
error(e);
}
/**
* Throws a {@link SAXParseException} containing details about all issues reported
* through {@link #error(SAXParseException)} or {@link #fatalError(SAXParseException)},
* the root cause being set to the first {@link SAXParseException} met.
*
* @throws SAXParseException see above
*/
public void throwSummary() throws SAXParseException {
if (firstException != null) {
throw new SAXParseException(messageBuffer.toString(), firstException.getPublicId(), firstException.getSystemId(), firstException.getLineNumber(), firstException.getColumnNumber(), firstException);
}
}
}
/** . */
private static final Logger log = LoggerFactory.getLogger(GateInResourcesSchemaValidator.class);
/** . */
public static final String GATEIN_RESOURCES_1_0_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_0";
/** . */
public static final String GATEIN_RESOURCES_1_1_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_1";
/** . */
public static final String GATEIN_RESOURCES_1_2_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_2";
/** . */
public static final String GATEIN_RESOURCES_1_3_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_3";
/** . */
public static final String GATEIN_RESOURCES_1_4_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_4";
/** . */
public static final String GATEIN_RESOURCES_1_5_SYSTEM_ID = "http://www.gatein.org/xml/ns/gatein_resources_1_5";
/** . */
private static final String GATEIN_RESOURCE_1_0_XSD_PATH = "gatein_resources_1_0.xsd";
/** . */
private static final String GATEIN_RESOURCE_1_1_XSD_PATH = "gatein_resources_1_1.xsd";
/** . */
private static final String GATEIN_RESOURCE_1_2_XSD_PATH = "gatein_resources_1_2.xsd";
/** . */
private static final String GATEIN_RESOURCE_1_3_XSD_PATH = "gatein_resources_1_3.xsd";
/** . */
private static final String GATEIN_RESOURCE_1_4_XSD_PATH = "gatein_resources_1_4.xsd";
/** . */
private static final String GATEIN_RESOURCE_1_5_XSD_PATH = "gatein_resources_1_5.xsd";
private static final Map<String, String> SYSTEM_ID_TO_XSD_PATH;
private static final Map<String, Integer> NAMESPACE_URI_ORDERING;
/** We validate since gatein_resources_1_5 including. */
private static final int VALIDATE_SINCE_NAMESPACE_URI_INDEX;
private static final GateInResourcesSchemaValidator VALIDATOR;
static {
/* How many entries will there be in SYSTEM_ID_TO_XSD_PATH and NAMESPACE_URI_ORDERING */
final int mapEntriesCount = 6;
final int initialCapacity = mapEntriesCount + mapEntriesCount/2 + 1;
Map<String, String> systemIdToResourcePath = new HashMap<String, String>(initialCapacity);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_0_SYSTEM_ID, GATEIN_RESOURCE_1_0_XSD_PATH);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_1_SYSTEM_ID, GATEIN_RESOURCE_1_1_XSD_PATH);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_2_SYSTEM_ID, GATEIN_RESOURCE_1_2_XSD_PATH);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_3_SYSTEM_ID, GATEIN_RESOURCE_1_3_XSD_PATH);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_4_SYSTEM_ID, GATEIN_RESOURCE_1_4_XSD_PATH);
systemIdToResourcePath.put(GATEIN_RESOURCES_1_5_SYSTEM_ID, GATEIN_RESOURCE_1_5_XSD_PATH);
SYSTEM_ID_TO_XSD_PATH = Collections.unmodifiableMap(systemIdToResourcePath);
Map<String, Integer> ordering = new HashMap<String, Integer>(initialCapacity);
int i = 0;
ordering.put(GATEIN_RESOURCES_1_0_SYSTEM_ID, Integer.valueOf(i++));
ordering.put(GATEIN_RESOURCES_1_1_SYSTEM_ID, Integer.valueOf(i++));
ordering.put(GATEIN_RESOURCES_1_2_SYSTEM_ID, Integer.valueOf(i++));
ordering.put(GATEIN_RESOURCES_1_3_SYSTEM_ID, Integer.valueOf(i++));
ordering.put(GATEIN_RESOURCES_1_4_SYSTEM_ID, Integer.valueOf(i++));
ordering.put(GATEIN_RESOURCES_1_5_SYSTEM_ID, Integer.valueOf(i));
/* WARNING: Do not put more items into ordering here. See below. */
/* we validate since gatein_resources_1_5 */
VALIDATE_SINCE_NAMESPACE_URI_INDEX = i++;
/* Add future SYSTEM_IDs to ordering here:
* e.g. ordering.put(GATEIN_RESOURCES_1_6_SYSTEM_ID, Integer.valueOf(i++));
* ... and do not forget to adjust mapEntriesCount above.
* */
NAMESPACE_URI_ORDERING = Collections.unmodifiableMap(ordering);
VALIDATOR = new GateInResourcesSchemaValidator();
}
/**
* Builds a DOM from a {@code gatein-resources.xml} and validates against GateIn resources XML schema, if the the root element's
* namespace URI is http://www.gatein.org/xml/ns/gatein_resources_1_5 or newer.
*
* @param url the URL to read {@code gatein-resources.xml} from.
* @return a DOM document
* @throws IOException if there are I/O problems.
* @throws SAXException on document validation problems.
*/
public static Document validate(URL url) throws IOException, SAXException {
return validate(DocumentSource.create(url));
}
/**
* Builds a DOM from a {@code gatein-resources.xml} and validates against GateIn resources XML schema, if the the root element's
* namespace URI is http://www.gatein.org/xml/ns/gatein_resources_1_5 or newer.
*
* @param source the {@link DocumentSource} to read from
* @return a DOM document
* @throws IOException if there are I/O problems.
* @throws SAXException on document validation problems.
*/
public static Document validate(DocumentSource source) throws IOException, SAXException {
return VALIDATOR.validateInternal(source);
}
/**
* Returns {@code true} if a document with the given {@code namespaceUri} should be validated
* or {@code false} otherwise. We return {@code true} for namespace URIs equal to
* http://www.gatein.org/xml/ns/gatein_resources_1_5 or newer.
*
* @param namespaceUri
* @return see above.
*/
private static boolean shouldValidate(String namespaceUri) {
Integer index = NAMESPACE_URI_ORDERING.get(namespaceUri);
boolean result = index != null && index >= VALIDATE_SINCE_NAMESPACE_URI_INDEX;
if (log.isDebugEnabled()) {
log.debug("Should validate with XML schema for namspace URI '"+ namespaceUri +"'? "+ result);
}
return result;
}
/**
* Performs some integrity checks.
*/
static void assertValid() {
/* all system IDs need to be valid namespace URIs
* but not namespace URIs need to be valid system IDs */
if (SYSTEM_ID_TO_XSD_PATH.size() < NAMESPACE_URI_ORDERING.size()) {
throw new IllegalStateException("All system IDs need to be valid namespace URIs but not all namespace URIs need to be valid system IDs");
}
for (String namespaceUri : NAMESPACE_URI_ORDERING.keySet()) {
if (SYSTEM_ID_TO_XSD_PATH.get(namespaceUri) == null) {
throw new IllegalStateException("All system IDs need to be valid namespace URIs but not all namespace URIs need to be valid system IDs");
}
}
}
/** A place to keep {@link Schema}s parsed once at startup. */
private final Map<String, Schema> namespaceUriToSchemaMap;
private GateInResourcesSchemaValidator() {
ClassLoader loader = getClass().getClassLoader();
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Map<String, Schema> nsSchemaMap = new HashMap<String, Schema>(NAMESPACE_URI_ORDERING.size());
try {
for (String namespaceUri : NAMESPACE_URI_ORDERING.keySet()) {
if (shouldValidate(namespaceUri)) {
String path = SYSTEM_ID_TO_XSD_PATH.get(namespaceUri);
URL url = loader.getResource(path);
if (url == null) {
throw new IllegalStateException("Cannot load a schema from path '"+ path +"'");
}
Schema schema = factory.newSchema(url);
nsSchemaMap.put(namespaceUri, schema);
}
}
this.namespaceUriToSchemaMap = Collections.unmodifiableMap(nsSchemaMap);
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
/**
* Builds a DOM from a {@code gatein-resources.xml} and validates against GateIn resources XML schema, if the the root element's
* namespace URI is http://www.gatein.org/xml/ns/gatein_resources_1_5 or newer.
*
* @param source the {@link DocumentSource} to read from
* @return a DOM document
* @throws IOException if there are I/O problems.
* @throws SAXException on document validation problems.
*/
private Document validateInternal(DocumentSource source) throws IOException, SAXException {
InputStream documentStream = null;
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setValidating(false);
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
documentStream = source.getStream();
Document result = builder.parse(documentStream);
Element root = result.getDocumentElement();
if (root != null) {
String namespaceURI = root.getNamespaceURI();
if (namespaceURI != null) {
Schema schema = namespaceUriToSchemaMap.get(namespaceURI);
if (schema != null) {
/* schema != null should implicitly mean that
* shouldValidate(namespaceUri) is true. */
Validator validator = schema.newValidator();
ErrorCollector errorCollector = new ErrorCollector(source.getIdentifier());
builder.setErrorHandler(errorCollector);
InputStream validationStream = null;
try {
validationStream = source.getStream();
/* We could actually use a DOMSource(result) here. It would perhaps be faster, but
* When with a StreamSource, line and column numbers are provided
* for every validation issue, which is better to debug */
validator.validate(new StreamSource(validationStream));
} finally {
Safe.close(validationStream);
}
errorCollector.throwSummary();
}
}
}
return result;
} catch (ParserConfigurationException e) {
/* Should never happen */
throw new RuntimeException(e);
} finally {
Safe.close(documentStream);
}
}
}