package cz.cuni.mff.peckam.java.origamist.jaxb;
import java.io.IOException;
import java.io.Reader;
import java.util.LinkedList;
import java.util.List;
import javax.xml.bind.JAXBException;
import javax.xml.bind.UnmarshalException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import cz.cuni.mff.peckam.java.origamist.jaxb.AdditionalTransforms.AdditionalTransform;
import cz.cuni.mff.peckam.java.origamist.jaxb.AdditionalTransforms.TransformLocation;
/**
* Process an input XML document by unmarshalling.
* <p>
*
* BindingsController is not re-entrant (i.e., it does not support simultaneous execution by multiple threads, but it is
* re-useable.
* <p>
*
* This package is based on a XML Schema versioning system from http://www.funkypeople.biz/knowledge/JavaXml-v2.zip .
*
* @author Sean Barnett
* @author Martin Pecka
*
* @param T The type of the unmarhalled object.
*/
public class BindingsController<T>
{
/** The bindings manager to fetch the transformations from. */
protected final BindingsManager bindingsManager;
/** The schema this controller handles. */
protected final SchemaInfo schema;
/** The parser that parses the XML file. */
protected SAXParser unmarshalParser;
/** The binding found for the actually processed document. */
protected Bindings<T> binding = null;
/**
* This map holds the additional transforms that are to be applied if working with an input file whose namespace is
* a key from this map. <code>null</code> key holds transforms to be executed for all input files.
*/
protected AdditionalTransforms additionalTransforms = new AdditionalTransforms();
/**
* Simple constructor.
*
* @param bindingsManager A reference to bindings manager.
* @param namespace The namespace of the schema this controller handles.
*
* @throws JAXBException If something goes wrong in configuring the controller.
*/
public BindingsController(BindingsManager bindingsManager, String namespace) throws JAXBException
{
this.bindingsManager = bindingsManager;
schema = bindingsManager.getSchema(namespace);
if (schema == null)
throw new JAXBException("Unsupported document namespace '" + namespace + "'");
}
/**
* Unmarshals the given XML file applying transforms to assure it did correspond to the newest version of its schema
* before the sole unmarshalling.
*
* @param xmlReader The reader that reads the XML file.
*
* @return The unmarshalled object.
*
* @throws JAXBException If there is a problem with the framework software or environment.
* @throws UnmarshalException If there is a problem during unmarshaling.
* @throws IOException If reading of the source XML failed.
*/
public T unmarshal(Reader xmlReader) throws JAXBException, UnmarshalException, IOException
{
try {
unmarshalParser = bindingsManager.createParser();
unmarshalParser.getXMLReader().setContentHandler(new NamespaceDetectingContentHandler());
try {
unmarshalParser.getXMLReader().parse(new InputSource(xmlReader));
} catch (ParseAbortedException e) {
// ignore this exception, it just signalizes that no more parsing is required
} catch (SAXException e) {
if (!(e.getCause() instanceof ParseAbortedException))
throw e;
}
// The parse() method triggered the NamespaceDetectingContentHandler, which detected the schema of the XML
// file, and set the appropriate unmarshal handler to the reader. After the parse has finished, we can query
// the result from the Bindings object.
return binding.getResult();
} catch (ParserConfigurationException e) {
throw new JAXBException(e.getMessage(), e);
} catch (SAXException e) {
throw new JAXBException(e.getMessage(), (e.getException() == null) ? e : e.getException());
}
}
/**
* Get the appropriate bindings implementation based upon the relative versions of the XML and Java (both deduced
* from namespaces).
*
* @param xmlNamespace The namespace (and therefore the version) of the XML to unmarshal.
*
* @return The appropriate binding.
*
* @throws BindingsFrameworkException If there is a problem with the framework software or environment.
*/
protected Bindings<T> getBindings(String xmlNamespace) throws JAXBException
{
boolean equalNamespaces = schema.getNamespace().equals(xmlNamespace);
List<AdditionalTransform> start = new LinkedList<AdditionalTransforms.AdditionalTransform>();
List<AdditionalTransform> beforeUnmarshaller = new LinkedList<AdditionalTransforms.AdditionalTransform>();
List<AdditionalTransform> end = new LinkedList<AdditionalTransforms.AdditionalTransform>();
List<AdditionalTransform> transforms = additionalTransforms.getTransforms(xmlNamespace, equalNamespaces, start,
beforeUnmarshaller, end);
if (equalNamespaces && transforms.size() == 0) {
// if two schemas are the same then simple JAXB bindings are ok
return new SimpleBindings<T>(bindingsManager, schema);
} else {
// if XML is an older version than Java then locate a transform (or sequence of transforms) from bindings
// manager and create a ParserTransformBindings around it
TransformInfo transform = null;
if (!equalNamespaces) {
transform = bindingsManager.getTransform(xmlNamespace, schema.getNamespace());
} else { // transforms.size() must be >0 for equal namespaces
if (start.size() > 0) {
transform = start.get(start.size() - 1).getTransform();
transforms.remove(start.remove(start.size() - 1));
} else if (beforeUnmarshaller.size() > 0) {
transform = beforeUnmarshaller.get(beforeUnmarshaller.size() - 1).getTransform();
transforms.remove(beforeUnmarshaller.remove(beforeUnmarshaller.size() - 1));
} else {
transform = end.get(end.size() - 1).getTransform();
transforms.remove(end.remove(end.size() - 1));
}
}
if (transform == null)
throw new JAXBException("Unsupported XML namespace '" + xmlNamespace + "'");
return new ParserTransformBindings<T>(bindingsManager, transform, start, beforeUnmarshaller, end);
}
}
/**
* Add an additional transform that is to be applied in addition to the transforms fetched from
* {@link BindingsManager}
*
* @param transform The additional transform. The fromSchema and toSchema will be ignored.
* @param location The location of the transform.
* @param execIfSchemataEqual Whether to run this transform even if the XML's schema is in the newest version.
* @param namespaces The list of source namespaces for which this transform has to be applied. A <code>null</code>
* namespace (or namespaces omitted at all) means this transform has to be executed for all source
* namespaces (beware: the setting of execIfSchemataEqual can disable the transform if the source
* namespace is equal to the target one).
*/
public void addAdditionalTransform(TransformInfo transform, TransformLocation location,
boolean execIfSchemataEqual, String... namespaces)
{
additionalTransforms.add(transform, location, execIfSchemataEqual, namespaces);
}
/**
* This content handler just reads the first tag, and if it is namespaced, sets the content handler appropriate for
* the found source schema to the parser's reader. If the first element is not namespaced, this handler throws a
* {@link org.xml.sax.SAXException}.
*
* @author Martin Pecka
*/
protected class NamespaceDetectingContentHandler extends DefaultHandler
{
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
if (uri.length() == 0)
throw new SAXException("Trying to unmarshal XML file without a namespaced root element.");
// based upon target XML version (learned from this.schema) and input XML version get the right bindings
// implementation
try {
binding = getBindings(uri);
} catch (JAXBException e) {
throw new SAXException(e);
}
// get the unmarshaller content handler
ContentHandler handler = binding.getContentHandler();
if (handler == null)
throw new SAXException("Couldn't find a handler for unmarshalling the given XML file.");
if (!uri.equals(schema.getNamespace())) {
Logger.getLogger(getClass()).info(
"Converted loaded XML file from schema " + uri + " to schema " + schema.getNamespace());
}
// this effectively removes this handler from the reader and substitutes it with the bindings' handler
unmarshalParser.getXMLReader().setContentHandler(handler);
// simulate the start of the parsing process for the new handler
// TODO maybe we have to handle startPrefixMapping() or similar methods
handler.startDocument();
handler.startElement(uri, localName, qName, attributes);
}
}
}