/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph.io.xml;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import ca.gedge.opgraph.OpGraph;
import ca.gedge.opgraph.extensions.Extendable;
import ca.gedge.opgraph.io.OpGraphSerializer;
import ca.gedge.opgraph.io.OpGraphSerializerInfo;
import ca.gedge.opgraph.util.ServiceDiscovery;
/**
* A factory that maps qualified names to serializers that handle them.
*/
@OpGraphSerializerInfo(extension="xml", description="XML Files")
public final class XMLSerializerFactory implements OpGraphSerializer {
/** The default namespace */
static final String DEFAULT_NAMESPACE = "http://gedge.ca/ns/opgraph";
/** The default prefix used for writing */
static final String DEFAULT_PREFIX = "og";
/** Logger */
private static final Logger LOGGER = Logger.getLogger(XMLSerializerFactory.class.getName());
/** The serializers to use */
private Collection<XMLSerializer> serializers;
/** XML Validator */
private Validator validator;
/**
* Default constructor.
*/
public XMLSerializerFactory() {
this.serializers = new ArrayList<XMLSerializer>();
initialize();
}
public void initialize() {
// Load XML serialization providers
serializers.clear();
for(Class<? extends XMLSerializer> provider : ServiceDiscovery.getInstance().findProviders(XMLSerializer.class)) {
try {
serializers.add( provider.newInstance() );
} catch(InstantiationException exc) {
LOGGER.warning("Could not instantiate XMLSerializer provider: " + provider.getName());
} catch(IllegalAccessException exc) {
LOGGER.warning("Could not instantiate XMLSerializer provider: " + provider.getName());
}
}
// Construct a validator
// XXX perhaps just do this once in a static block/function?
validator = null;
try {
// Find a list of all schemas
final List<URL> schemaLists = ServiceDiscovery.getInstance().findResources("META-INF/schemas/list");
final List<URL> schemas = new ArrayList<URL>();
for(URL schemaListURL : schemaLists) {
final BufferedReader br = new BufferedReader(new InputStreamReader(schemaListURL.openStream()));
String line = null;
while((line = br.readLine()) != null)
schemas.addAll( ServiceDiscovery.getInstance().findResources("META-INF/schemas/" + line) );
}
// Load up extension schemas
final Source [] schemaSource = new Source[schemas.size() + 1];
for(int index = 0; index < schemas.size(); ++index)
schemaSource[index + 1] = new StreamSource(schemas.get(index).openStream());
// Ensure core OpGraph schema comes first
schemaSource[0] = new StreamSource(XMLSerializerFactory.class.getResource("/META-INF/schemas/opgraph.xsd").openStream());
final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
final Schema schema = sf.newSchema(schemaSource);
validator = schema.newValidator();
} catch(SAXException exc) {
LOGGER.warning("SAXException while initializing validator: " + exc.getLocalizedMessage());
} catch(IOException exc) {
LOGGER.warning("IOException while initializing validator: " + exc.getLocalizedMessage());
}
}
/**
* Gets the qualified name of an element.
*
* @param elem the element
*
* @return the qualified name of the element
*/
public static QName getQName(Element elem) {
String localName = elem.getLocalName();
String prefix = (elem.getPrefix() == null ? XMLConstants.DEFAULT_NS_PREFIX : elem.getPrefix());
return new QName(elem.getNamespaceURI(), localName, prefix);
}
/**
* Writes an element's extensions to a parent element.
*
* @param doc the document
* @param parent the parent element to write to
* @param ext the {@link Extendable}
*
* @throws IOException if any errors occur when serializing
*/
public void writeExtensions(Document doc, Element parent, Extendable ext) throws IOException {
final Element extensionsElem = doc.createElementNS(DEFAULT_NAMESPACE, "extensions");
for(Class<?> extension : ext.getExtensionClasses()) {
final XMLSerializer serializer = getHandler(extension);
if(serializer == null)
LOGGER.warning("Node contains an unwritable extension: " + extension.getName());
else
serializer.write(this, doc, extensionsElem, ext.getExtension(extension));
}
if(extensionsElem.getChildNodes().getLength() > 0)
parent.appendChild(extensionsElem);
}
/**
* Gets the handler for a specified qualified name.
*
* @param name qualified name for which a serializer is needed
*
* @return an XML serializer for the given qualified name, or <code>null</code>
* if no handler is registered for the given qualified name
*/
public XMLSerializer getHandler(QName name) {
for(XMLSerializer serializer : serializers) {
if(serializer.handles(name))
return serializer;
}
return null;
}
/**
* Gets the handler for a specified class. Ascends the inheritance chain
* of the given class to see if there is a handler for a super class.
*
* @param cls class for which a serializer is needed
*
* @return an XML serializer for the given class, or <code>null</code> if
* no handler is registered for the class
*/
public XMLSerializer getHandler(Class<?> cls) {
while(cls != null) {
for(XMLSerializer serializer : serializers) {
if(serializer.handles(cls))
return serializer;
}
cls = cls.getSuperclass();
}
return null;
}
//
// Overrides
//
/**
* Writes a graph to a stream.
*
* @param graph the graph to write
* @param stream the stream to write to
*
* @throws IOException if any I/O errors occur
*/
@Override
public void write(OpGraph graph, OutputStream stream) throws IOException {
Document doc;
try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
// Construct a DOM document
final DocumentBuilder docBuilder = factory.newDocumentBuilder();
final DOMImplementation domImpl = docBuilder.getDOMImplementation();
// use NAMESPACE as default namespace for document
doc = domImpl.createDocument(DEFAULT_NAMESPACE, "opgraph", null);
} catch(ParserConfigurationException exc) {
throw new IOException("Could not create document builder", exc);
}
final Element root = doc.getDocumentElement();
final XMLSerializer serializer = getHandler(graph.getClass());
if(serializer != null)
serializer.write(this, doc, root, graph);
doc.normalize();
// Write to stream
try {
final Source source = new DOMSource(doc);
final Result result = new StreamResult(stream);
final Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
transformer.transform(source, result);
} catch(TransformerConfigurationException exc) {
throw new IOException("Could not write DOM tree to stream", exc);
} catch(TransformerFactoryConfigurationError exc) {
throw new IOException("Could not write DOM tree to stream", exc);
} catch(TransformerException exc) {
throw new IOException("Could not write DOM tree to stream", exc);
}
}
/**
* Reads a graph from a stream.
*
* @param stream the stream to read from
*
* @throws IOException if any I/O errors occur
*/
@Override
public OpGraph read(InputStream stream) throws IOException {
// Create document
Document doc;
try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
doc = factory.newDocumentBuilder().parse(stream);
} catch(SAXException exc) {
throw new IOException("Could not parse stream as XML", exc);
} catch(ParserConfigurationException exc) {
throw new IOException("Could not create document builder", exc);
}
// XXX Should we require a validator?
if(validator != null) {
try {
final Source source = new DOMSource(doc);
final DOMResult result = new DOMResult();
validator.validate(source, result);
// Get the schema-transformed document
final Node resultNode = result.getNode();
if(resultNode instanceof Document)
doc = (Document)resultNode;
} catch(SAXException exc) {
exc.printStackTrace();
throw new IOException("Given stream is not a valid OpGraph XML document", exc);
}
}
// Read from stream
OpGraph ret = null;
final XMLSerializer serializer = getHandler(getQName(doc.getDocumentElement()));
if(serializer != null) {
final Object objRead = serializer.read(this, null, null, doc, doc.getDocumentElement());
if(objRead instanceof OpGraph)
ret = (OpGraph)objRead;
}
if(ret == null)
throw new IOException("Graph could not be read from stream");
return ret;
}
}