/* * 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(); } } }