package cz.cuni.mff.peckam.java.origamist.jaxb;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.TreeMap;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.stream.StreamSource;
import org.xml.sax.SAXException;
/**
* A combination of an XML schema catalogue, an XML transformations catalogue
* and a holder for appropriately configured parser and transform factories.
* <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
*/
public class BindingsManager
{
/** The JAXB context to work with. */
protected final JAXBContext jaxbContext;
/**
* The factory for creating {@link javax.xml.transform.sax.TransformerHandler} objects from
* {@link javax.xml.transform.Transformer}s.
*/
protected CustomTransformerFactory transformerFactory;
/** The factory for creating parsers. */
protected SAXParserFactory parserFactory;
/** A mapping from namespaces to corresponding schemata. */
protected Map<String, SchemaInfo> schemaByNamespace = new TreeMap<String, SchemaInfo>();
/**
* A mapping from a schema (identified by namespace) to a mapping of schemata it can be transformed to, the latter
* mapping pointing to the {@link TransformInfo} needed for such transformation.
*/
protected Map<String, Map<String, TransformInfo>> transformsByFromNamespace = new TreeMap<String, Map<String, TransformInfo>>();
/**
* A mapping from a schema (identified by namespace) to a callback that configures the {@link Unmarshaller} created
* for that schema.
*/
protected Map<String, UnmarshallerConfigurator> unmarshallerConfigurators = new TreeMap<String, UnmarshallerConfigurator>();
/**
* Simple constructor.
*
* @param jaxbContext The pre-created JAXB context. Caller should use either <code>new ObjectFactory()</code> -
* using ObjectFactory from the appropriate Java package, or more usually the
* {@link JAXBContext#newInstance(Class...)} method. Latter is more useful because it can accommodate
* multiple bound Java packages (as parameter to newInstance()).
*/
public BindingsManager(JAXBContext jaxbContext)
{
this.jaxbContext = jaxbContext;
transformerFactory = new CustomTransformerFactory();
parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
}
/**
* @return The JAXBContext, as passed into constructor.
*/
public JAXBContext getJaxbContext()
{
return jaxbContext;
}
/**
* @return The factory for transformers.
*/
public CustomTransformerFactory getTransformerFactory()
{
return transformerFactory;
}
/**
* Lookup an XML Schema based upon its namespace.
*
* @param namespace The namespace to find schema for. This namespace must have been added to BindingsManager using
* {@link addSchema} earlier.
*
* @return The {@link SchemaInfo} for the given namespace, or <code>null</code> if no schema with the given
* namespace has been registered.
*/
public SchemaInfo getSchema(String namespace)
{
return schemaByNamespace.get(namespace);
}
/**
* Find a transform definition for moving from one XML schema to another. Such a transform must have been added
* using {@link addTransform} earlier.
*
* @param xmlNamespace The "target namespace" of the source XML schema.
* @param javaNamespace The "target namespace" of the destination XML schema,
*
* @return The transform for transition between schemata defined by the two namespaces.
*/
public TransformInfo getTransform(String xmlNamespace, String javaNamespace)
{
// transforms are indexed first by source namespace and then by
// destination namespace
Map<String, TransformInfo> templatesByToNamespace = transformsByFromNamespace.get(xmlNamespace);
if (templatesByToNamespace != null) {
return templatesByToNamespace.get(javaNamespace);
}
return null;
}
/**
* Add a schema definition to the bindings manager.
*
* @param namespace The target namespace, effectively a primary key for the schema.
* @param location The actual location of the schema as a Java resource (i.e., effectively a file that exists within
* the classpath somewhere)
* @param bound If true indicates that JAXB bindings exist for this schema.
*
* @return The created {@link SchemaInfo}.
*
* @throws IOException If the schema could not be read from <code>location</code>.
*/
public SchemaInfo addSchema(String namespace, String location, boolean bound) throws IOException
{
InputStream instream = getClass().getClassLoader().getResourceAsStream(location);
if (instream == null)
throw new IOException("Cannot open schema resource '" + location + "'");
String schema = readToString(instream);
SchemaInfo result = new SchemaInfo(namespace, location, schema, bound);
schemaByNamespace.put(namespace, result);
return result;
}
/**
* Add a transform definition. The nominated XML transform is pre-compiled at this point.
*
* @param fromSchema The schema the transform goes from.
* @param toSchema The schema the transform goes to.
* @param location The resource where the XML transform is located (i.e., effectively a file in the classpath).
* @param parent The transform that should be applied before that at <code>location</code>.
*
* @return The newly created {@link TransformInfo}.
*
* @throws IOException If the transform cannot be read from <code>location</code>.
* @throws TransformerConfigurationException If there are errors in the transform definition.
*/
public TransformInfo addTransform(SchemaInfo fromSchema, SchemaInfo toSchema, String location, TransformInfo parent)
throws IOException, TransformerConfigurationException
{
InputStream instream = getClass().getClassLoader().getResourceAsStream(location);
if (instream == null)
throw new IOException("Cannot open transform resource '" + location + "'");
// pre-compile transform
Templates templates = transformerFactory.newTemplates(new StreamSource(instream));
TransformInfo result = new TransformInfo(parent, fromSchema, toSchema, templates);
doAddTransform(result);
return result;
}
/**
* Add a transform definition.
*
* @param fromSchema The schema the transform goes from.
* @param toSchema The schema the transform goes to.
* @param transform The transform to add.
*
* @return <code>transform</code>
*/
public TransformInfo addTransform(SchemaInfo fromSchema, SchemaInfo toSchema, TransformInfo transform)
{
doAddTransform(transform);
return transform;
}
/**
* Add a new transform that is a result of combining two previously defined transforms. For example, if transform X
* goes from schema A to B, and transform Y goes from B to C, then newly created transform Z will go from schema A
* to schema C.
*
* @param first The first transform.
* @param second The second transform.
*
* @return The newly created transform.
*
* @throws IllegalArgumentException If the target of the first transform is notthe source of the second transform.
*/
public TransformInfo addTransform(TransformInfo first, TransformInfo second) throws IllegalArgumentException
{
if (!first.getToSchema().equals(second.getFromSchema()))
throw new IllegalArgumentException("Two transforms are not additive");
TransformInfo result;
if (second.getTemplates() != null) {
result = new TransformInfo(first, first.getFromSchema(), second.getToSchema(), second.getTemplates());
} else {
result = new TransformInfo(first, first.getFromSchema(), second.getToSchema(), second.getContentHandler());
}
doAddTransform(result);
return result;
}
/**
* Set the given unmarshaller configurater to be used for {@link Unmarshaller}s created for the given schema.
*
* @param schema
* @param configurator
*
* @throws IllegalArgumentException If the given schema doesn't have a JAXB binding (
* <code>schema.isBound == false</code>).
*/
public void addUnmarshallerConfigurator(SchemaInfo schema, UnmarshallerConfigurator configurator)
throws IllegalArgumentException
{
if (!schema.isBound())
throw new IllegalArgumentException(
"Cannot set a unmarshaller configurator for a schema which has no JAXB binding.");
unmarshallerConfigurators.put(schema.getNamespace(), configurator);
}
/**
* Create a SAX parser to process a generic XML file.
*
* @return A SAX parser to process generic XML files.
*
* @throws ParserConfigurationException If a parser cannot be created which satisfies the requested configuration.
* @throws SAXException For SAX errors.
*/
public SAXParser createParser() throws ParserConfigurationException, SAXException
{
return createParser(null);
}
/**
* Create a SAX parser to process XML according to the given XML schema.
*
* @param schema The schema to create parser for. If <code>null</code>, a generic XML parser is returned.
*
* @return A SAX parser to process XML according to the given XML schema.
*
* @throws ParserConfigurationException If a parser cannot be created which satisfies the requested configuration.
* @throws SAXException For SAX errors.
*/
public SAXParser createParser(SchemaInfo schema) throws ParserConfigurationException, SAXException
{
// yet we don't have any custom needs to differentiate the parsers according to their schemata
return parserFactory.newSAXParser();
}
/**
* Return an {@link Unmarshaller} able to unmarshal the given schema.
*
* @param schema The schema to get unmarshaller for.
* @return An {@link Unmarshaller} able to unmarshal the given schema.
*
* @throws JAXBException If an error was encountered while creating the Unmarshaller object.
*/
public Unmarshaller createUnmarshaller(SchemaInfo schema) throws JAXBException
{
Unmarshaller result = getJaxbContext().createUnmarshaller();
UnmarshallerConfigurator configurator = unmarshallerConfigurators.get(schema.getNamespace());
if (configurator != null) {
configurator.configure(result);
}
return result;
}
/**
* Index a transform, first by source XML schema and secondly by target XML schema.
*
* @param transform The transform to index.
*/
protected void doAddTransform(TransformInfo transform)
{
Map<String, TransformInfo> byToNamespace = transformsByFromNamespace.get(transform.getFromSchema()
.getNamespace());
if (byToNamespace == null) {
byToNamespace = new TreeMap<String, TransformInfo>();
transformsByFromNamespace.put(transform.getFromSchema().getNamespace(), byToNamespace);
}
byToNamespace.put(transform.getToSchema().getNamespace(), transform);
}
/**
* Reads the contents of the input stream into a string.
*
* This method doesn't close the stream.
*
* @param instream The stream to read.
* @return The string with all the contents of the stream loaded into it.
*
* @throws IOException If the reading failed.
*/
protected final String readToString(InputStream instream) throws IOException
{
StringBuffer result = new StringBuffer();
byte[] buffer = new byte[4096];
for (int didRead = 0; (didRead = instream.read(buffer)) >= 0;) {
result.append(new String(buffer, 0, didRead));
}
return result.toString();
}
}