/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.xml.transform;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;
import org.xml.sax.helpers.XMLFilterImpl;
/**
* TransformerBase provides support for writing Object->XML encoders. The basic
* pattern for useage is to extend TransformerBase and implement the
* createTranslator(ContentHandler) method. This is easiest done by extending
* the inner class TranslatorSupport. A Translator uses a ContentHandler to
* issue SAX events to a javax.xml.transform.Transformer. If possible, make
* the translator public so it can be used by others as well.
*
* @author Ian Schneider
*
* @source $URL$
*/
public abstract class TransformerBase {
private int indentation = -1;
private boolean xmlDecl = false;
private boolean nsDecl = true;
private Charset charset = Charset.forName("UTF-8");
public static final String XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
/**
* Create a Translator to issue SAXEvents to a ContentHandler.
*/
public abstract Translator createTranslator(ContentHandler handler);
/**
* Create a Transformer which is initialized with the settings of this
* TransformerBase.
*/
public Transformer createTransformer() throws TransformerException {
TransformerFactory tFactory = TransformerFactory.newInstance();
if (indentation > -1) {
try {
tFactory.setAttribute("indent-number", new Integer(indentation));
} catch (IllegalArgumentException e) {
//throw away (java 1.4 doesn't support this method, but java 1.5 requires it)
}
}
Transformer transformer = tFactory.newTransformer();
if (indentation > -1) {
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", Integer.toString(indentation));
} else {
transformer.setOutputProperty(OutputKeys.INDENT, "no");
}
if (xmlDecl) {
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
} else {
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
}
transformer.setOutputProperty(OutputKeys.ENCODING, charset.name());
return transformer;
}
/**
* Perform the XML encoding on the given object to the given OutputStream.
* Calls transform(Object,StreamResult);
*/
public void transform(Object object, java.io.OutputStream out)
throws TransformerException {
transform(object, new StreamResult(out));
}
/**
* Perform the XML encoding on the given object to the given Writer. Calls
* transform(Object,StreamResult);
*/
public void transform(Object object, java.io.Writer out)
throws TransformerException {
transform(object, new StreamResult(out));
}
/**
* Perform the XML encoding on the given object to the given OutputStream.
* Calls createTransformer(),createXMLReader() and
* Transformer.transform().
*/
public void transform(Object object, StreamResult result)
throws TransformerException {
Task t = createTransformTask(object,result);
t.run();
if (t.checkError()) {
Exception e = t.getError();
if (! TransformerException.class.isAssignableFrom(e.getClass())) {
e = new TransformerException("Translator error",e);
}
throw (TransformerException) e;
}
}
/** Create a Transformation task. This is a Runnable task
* which supports aborting any processing. It will not start until the
* run method is called.
*
*/
public Task createTransformTask(Object object,StreamResult result)
throws TransformerException {
return new Task(object,result);
}
/**
* Perform the XML encoding of the given object into an internal buffer and
* return the resulting String. Calls transform(Object,Writer). <em>It
* should be noted the most efficient mechanism of encoding is using the
* OutputStream or Writer methods</em>
*/
public String transform(Object object) throws TransformerException {
StringWriter sw = new StringWriter();
transform(object,sw);
return sw.getBuffer().toString();
}
/**
* Create an XMLReader to use in the transformation.
*/
public XMLReaderSupport createXMLReader(Object object) {
return new XMLReaderSupport(this,object);
}
/**
* Get the number of spaces to indent the output xml. Defaults to -1.
*
* @return The number of spaces to indent, or -1, to disable.
*/
public int getIndentation() {
return indentation;
}
/**
* Set the number of spaces to indent the output xml. Default to -1.
*
* @param amt The number of spaces to indent if > 0, otherwise disable.
*/
public void setIndentation(int amt) {
indentation = amt;
}
/**
* Gets the charset to declare in the header of the response.
* @return the charset to encode with.
*/
public Charset getEncoding() {
return charset;
}
/**
* Sets the charset to declare in the xml header returned.
*
* @param charset A charset object of the desired encoding
*/
public void setEncoding(Charset charset){
this.charset = charset;
}
/**
* Will this transformation omit the standard XML declaration. <b>Defaults
* to false</b>
*
* @return true if the XML declaration will be omitted, false otherwise.
*/
public boolean isOmitXMLDeclaration() {
return xmlDecl;
}
/**
* Set this transformer to omit/include the XML declaration. <b>Defaults to
* false</b>
*
* @param xmlDecl Omit/include the XML declaration.
*/
public void setOmitXMLDeclaration(boolean xmlDecl) {
this.xmlDecl = xmlDecl;
}
/**
* Should this transformer declare namespace prefixes in the first element
* it outputs? Defaults to true.
*
* @return true if namespaces will be declared, false otherwise
*/
public boolean isNamespaceDeclartionEnabled() {
return nsDecl;
}
/**
* Enable declaration of namespace prefixes in the first element. Defaults
* to true;
*
* @param enabled Enable namespace declaration.
*/
public void setNamespaceDeclarationEnabled(boolean enabled) {
nsDecl = enabled;
}
/** A wrapper for a Transformation Task. Support aborting any translation
* activity. Because the Task is Runnable, exceptions must be checked
* asynchronously by using the checkError and getError methods.
*/
public class Task implements Runnable {
//private final Translator translator;
private final Transformer transformer;
private final Source xmlSource;
private final StreamResult result;
private final XMLReaderSupport reader;
private Exception error;
public Task(Object object,StreamResult result) throws TransformerException {
transformer = createTransformer();
reader = createXMLReader(object);
xmlSource = new SAXSource(createXMLReader(object),
new InputSource());
this.result = result;
}
/** Did an error occur?
* @return true if one did, false otherwise.
*/
public boolean checkError() {
return error != null;
}
/** Get any error which occurred.
* @return An Exception if checkError returns true, null otherwise.
*/
public Exception getError() {
return error;
}
/** Calls to the underlying translator to abort any calls to translation.
* Should return silently regardless of outcome.
*/
public void abort() {
Translator t = reader.getTranslator();
if (t != null)
t.abort();
}
/**
* Perform the translation. Exceptions are captured and can be obtained
* through the checkError and getError methods.
*/
public void run() {
try {
transformer.transform(xmlSource, result);
} catch (Exception re) {
error = re;
}
}
}
/**
* Filter output from a ContentHandler and insert Namespace declarations in
* the first element.
*/
private static class ContentHandlerFilter implements ContentHandler, LexicalHandler {
private final ContentHandler original;
private AttributesImpl namespaceDecls;
public ContentHandlerFilter(ContentHandler original,
AttributesImpl nsDecls) {
this.original = original;
this.namespaceDecls = nsDecls;
}
public void characters(char[] ch, int start, int length)
throws SAXException {
original.characters(ch, start, length);
}
public void endDocument() throws SAXException {
original.endDocument();
}
public void endElement(String namespaceURI, String localName,
String qName) throws SAXException {
original.endElement(namespaceURI, localName, qName);
}
public void endPrefixMapping(String prefix) throws SAXException {
original.endPrefixMapping(prefix);
}
public void ignorableWhitespace(char[] ch, int start, int length)
throws SAXException {
original.ignorableWhitespace(ch, start, length);
}
public void processingInstruction(String target, String data)
throws SAXException {
original.processingInstruction(target, data);
}
public void setDocumentLocator(org.xml.sax.Locator locator) {
original.setDocumentLocator(locator);
}
public void skippedEntity(String name) throws SAXException {
original.skippedEntity(name);
}
public void startDocument() throws SAXException {
original.startDocument();
}
public void startElement(String namespaceURI, String localName,
String qName, Attributes atts) throws SAXException {
if (namespaceDecls != null) {
for (int i = 0, ii = atts.getLength(); i < ii; i++) {
namespaceDecls.addAttribute(null, null, atts.getQName(i),
atts.getType(i), atts.getValue(i));
}
atts = namespaceDecls;
namespaceDecls = null;
}
if (namespaceURI == null)
namespaceURI = "";
if (localName == null)
localName = "";
original.startElement(namespaceURI, localName, qName, atts);
}
public void startPrefixMapping(String prefix, String uri)
throws SAXException {
original.startPrefixMapping(prefix, uri);
}
public void comment(char[] ch, int start, int length) throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).comment(ch, start, length);
}
}
public void startCDATA() throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).startCDATA();
}
}
public void endCDATA() throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).endCDATA();
}
}
public void startDTD(String name, String publicId, String systemId) throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).startDTD(name, publicId, systemId);
}
}
public void endDTD() throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).endDTD();
}
}
public void startEntity(String name) throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).startEntity(name);
}
}
public void endEntity(String name) throws SAXException {
if ( original instanceof LexicalHandler ) {
((LexicalHandler)original).endEntity(name);
}
}
}
/**
* Support for writing Translators.
*/
protected abstract static class TranslatorSupport implements Translator {
protected final ContentHandler contentHandler;
private String prefix;
private String namespace;
protected final Attributes NULL_ATTS = new AttributesImpl();
protected NamespaceSupport nsSupport = new NamespaceSupport();
protected SchemaLocationSupport schemaLocation;
/**
* Subclasses should check this flag in case an abort message was sent
* and stop any internal iteration if false.
*/
protected volatile boolean running = true;
public TranslatorSupport(ContentHandler contentHandler, String prefix,
String nsURI) {
this.contentHandler = contentHandler;
this.prefix = prefix;
this.namespace = nsURI;
if (prefix != null && nsURI != null)
nsSupport.declarePrefix(prefix, nsURI);
}
public TranslatorSupport(ContentHandler contentHandler, String prefix,
String nsURI, SchemaLocationSupport schemaLocation) {
this(contentHandler, prefix, nsURI);
this.schemaLocation = schemaLocation;
}
public void abort() {
running = false;
}
/**
* Utility method to copy namespace declarations from "sub" translators
* into this ns support...
*/
protected void addNamespaceDeclarations(TranslatorSupport trans) {
NamespaceSupport additional = trans.getNamespaceSupport();
java.util.Enumeration declared = additional.getDeclaredPrefixes();
while (declared.hasMoreElements()) {
String prefix1 = declared.nextElement().toString();
nsSupport.declarePrefix(prefix1, additional.getURI(prefix1));
}
}
/**
* Utility method for creating attributes from an array of name value
* pairs.
* <p>
* The <tt>nameValuePairs</tt> array should be of the form:
* <pre>{name1,value1,name2,value2,...,nameN,valueN}</pre>
* </p>
* @param nameValuePairs The attribute names/values.
*
*/
protected AttributesImpl createAttributes( String[] nameValuePairs) {
AttributesImpl attributes = new AttributesImpl();
for (int i = 0; i < nameValuePairs.length; i += 2) {
String name = nameValuePairs[i];
String value = nameValuePairs[i + 1];
attributes.addAttribute("", name, name, "", value);
}
return attributes;
}
protected void element(String element, String content) {
element(element, content, NULL_ATTS);
}
/**
* Will only issue the provided element if content is non empty
* @param element
* @param content
*/
protected void elementSafe( String element, String content){
if( content != null && content.length() != 0){
element(element, content, NULL_ATTS);
}
}
protected void element(String element, String content, Attributes atts) {
start(element, atts);
if (content != null) {
chars(content);
}
end(element);
}
protected void start(String element) {
start(element, NULL_ATTS);
}
protected void start(String element, Attributes atts) {
try {
String el = (prefix == null) ? element : (prefix + ":"
+ element);
contentHandler.startElement("", "", el, atts);
} catch (SAXException se) {
throw new RuntimeException(se);
}
}
protected void chars(String text) {
try {
char[] ch = text.toCharArray();
contentHandler.characters(ch, 0, ch.length);
} catch (SAXException se) {
throw new RuntimeException(se);
}
}
protected void end(String element) {
try {
String el = (prefix == null) ? element : (prefix + ":"
+ element);
contentHandler.endElement("", "", el);
} catch (SAXException se) {
throw new RuntimeException(se);
}
}
protected void cdata( String cdata ) {
if ( contentHandler instanceof LexicalHandler ) {
LexicalHandler lexicalHandler = (LexicalHandler) contentHandler;
try {
lexicalHandler.startCDATA();
chars(cdata);
lexicalHandler.endCDATA();
}
catch( SAXException e ) {
throw new RuntimeException( e );
}
}
}
public String getDefaultNamespace() {
return namespace;
}
public String getDefaultPrefix() {
return prefix;
}
public NamespaceSupport getNamespaceSupport() {
return nsSupport;
}
public SchemaLocationSupport getSchemaLocationSupport() {
return schemaLocation;
}
}
/**
* Adds support for schemaLocations.
*
* @task REVISIT: consider combining this with NamespaceSupport, as it
* would make sense to just add a location to your nsURI. Or at
* least tie them more closely, so that you can only add a
* SchemaLocation if the namespace actually exists.
*/
public static class SchemaLocationSupport {
private Map locations = new HashMap();
public void setLocation(String nsURI, String uri) {
locations.put(nsURI, uri);
}
public String getSchemaLocation() {
return getSchemaLocation(locations.keySet());
}
public String getSchemaLocation(String nsURI) {
String uri = (String) locations.get(nsURI);
if (uri == null) {
return "";
}
return nsURI + " " + uri;
}
public String getSchemaLocation(Set namespaces) {
StringBuffer location = new StringBuffer();
for (Iterator it = namespaces.iterator(); it.hasNext();) {
location.append(getSchemaLocation((String) it.next()));
if (it.hasNext()) {
location.append(" "); //\n? Pretty printing?
}
}
return location.toString();
}
}
/**
* Support for the setup of an XMLReader for use in a transformation.
*/
protected static class XMLReaderSupport extends XMLFilterImpl {
TransformerBase base;
Object object;
Translator translator;
public XMLReaderSupport(TransformerBase transfomerBase, Object object) {
this.base = transfomerBase;
this.object = object;
}
public final Translator getTranslator() {
return translator;
}
public void parse(InputSource in) throws SAXException {
ContentHandler handler = getContentHandler();
if (base.isNamespaceDeclartionEnabled()) {
AttributesImpl atts = new AttributesImpl();
ContentHandlerFilter filter = new ContentHandlerFilter(handler,
atts);
translator = base.createTranslator(filter);
if ( translator.getDefaultNamespace() != null ) {
//declare the default mapping
atts.addAttribute(XMLNS_NAMESPACE, null,
"xmlns", "CDATA", translator.getDefaultNamespace());
//if prefix non-null, declare the mapping
if( translator.getDefaultPrefix() != null ) {
atts.addAttribute(XMLNS_NAMESPACE, null,
"xmlns:" + translator.getDefaultPrefix(), "CDATA",
translator.getDefaultNamespace());
}
}
NamespaceSupport ns = translator.getNamespaceSupport();
java.util.Enumeration e = ns.getPrefixes();
//TODO: only add schema locations for namespaces that are
//actually here, or some sort of better checking.
//Set namespaces = new HashSet();
while (e.hasMoreElements()) {
String prefix = e.nextElement().toString();
if (prefix.equals("xml")) {
continue;
}
String xmlns = "xmlns:" + prefix;
if (atts.getValue(xmlns) == null) {
atts.addAttribute(XMLNS_NAMESPACE, null, xmlns, "CDATA",
ns.getURI(prefix));
//namespaces.add(ns.getURI(prefix));
}
}
String defaultNS = ns.getURI("");
if ((defaultNS != null) && (atts.getValue("xmlns:") == null)) {
atts.addAttribute(XMLNS_NAMESPACE, null, "xmlns:", "CDATA", defaultNS);
//namespaces.add(defaultNS);
}
SchemaLocationSupport schemaLocSup = translator
.getSchemaLocationSupport();
if ((schemaLocSup != null)
&& !schemaLocSup.getSchemaLocation().equals("")) {
atts.addAttribute(XMLNS_NAMESPACE, null, "xmlns:xsi", "CDATA",
"http://www.w3.org/2001/XMLSchema-instance");
atts.addAttribute(null, null, "xsi:schemaLocation", null,
schemaLocSup.getSchemaLocation());
}
} else {
translator = base.createTranslator(handler);
}
handler.startDocument();
translator.encode(object);
handler.endDocument();
}
}
}