001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.xml.transform;
017:
018: import java.io.StringWriter;
019: import java.nio.charset.Charset;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.Set;
024:
025: import javax.xml.transform.OutputKeys;
026: import javax.xml.transform.Source;
027: import javax.xml.transform.Transformer;
028: import javax.xml.transform.TransformerException;
029: import javax.xml.transform.TransformerFactory;
030: import javax.xml.transform.sax.SAXSource;
031: import javax.xml.transform.stream.StreamResult;
032:
033: import org.xml.sax.Attributes;
034: import org.xml.sax.ContentHandler;
035: import org.xml.sax.InputSource;
036: import org.xml.sax.SAXException;
037: import org.xml.sax.ext.LexicalHandler;
038: import org.xml.sax.helpers.AttributesImpl;
039: import org.xml.sax.helpers.NamespaceSupport;
040: import org.xml.sax.helpers.XMLFilterImpl;
041:
042: /**
043: * TransformerBase provides support for writing Object->XML encoders. The basic
044: * pattern for useage is to extend TransformerBase and implement the
045: * createTranslator(ContentHandler) method. This is easiest done by extending
046: * the inner class TranslatorSupport. A Translator uses a ContentHandler to
047: * issue SAX events to a javax.xml.transform.Transformer. If possible, make
048: * the translator public so it can be used by others as well.
049: *
050: * @author Ian Schneider
051: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/xml/transform/TransformerBase.java $
052: */
053: public abstract class TransformerBase {
054: private int indentation = -1;
055: private boolean xmlDecl = false;
056: private boolean nsDecl = true;
057: private Charset charset = Charset.forName("UTF-8");
058:
059: public static final String XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
060:
061: /**
062: * Create a Translator to issue SAXEvents to a ContentHandler.
063: */
064: public abstract Translator createTranslator(ContentHandler handler);
065:
066: /**
067: * Create a Transformer which is initialized with the settings of this
068: * TransformerBase.
069: */
070: public Transformer createTransformer() throws TransformerException {
071: TransformerFactory tFactory = TransformerFactory.newInstance();
072: if (indentation > -1) {
073: try {
074: tFactory.setAttribute("indent-number", new Integer(
075: indentation));
076: } catch (IllegalArgumentException e) {
077: //throw away (java 1.4 doesn't support this method, but java 1.5 requires it)
078: }
079: }
080:
081: Transformer transformer = tFactory.newTransformer();
082:
083: if (indentation > -1) {
084: transformer.setOutputProperty(OutputKeys.INDENT, "yes");
085: transformer.setOutputProperty(
086: "{http://xml.apache.org/xslt}indent-amount",
087: Integer.toString(indentation));
088: } else {
089: transformer.setOutputProperty(OutputKeys.INDENT, "no");
090: }
091:
092: if (xmlDecl) {
093: transformer.setOutputProperty(
094: OutputKeys.OMIT_XML_DECLARATION, "yes");
095: } else {
096: transformer.setOutputProperty(
097: OutputKeys.OMIT_XML_DECLARATION, "no");
098: }
099:
100: transformer.setOutputProperty(OutputKeys.ENCODING, charset
101: .name());
102:
103: return transformer;
104: }
105:
106: /**
107: * Perform the XML encoding on the given object to the given OutputStream.
108: * Calls transform(Object,StreamResult);
109: */
110: public void transform(Object object, java.io.OutputStream out)
111: throws TransformerException {
112: transform(object, new StreamResult(out));
113: }
114:
115: /**
116: * Perform the XML encoding on the given object to the given Writer. Calls
117: * transform(Object,StreamResult);
118: */
119: public void transform(Object object, java.io.Writer out)
120: throws TransformerException {
121: transform(object, new StreamResult(out));
122: }
123:
124: /**
125: * Perform the XML encoding on the given object to the given OutputStream.
126: * Calls createTransformer(),createXMLReader() and
127: * Transformer.transform().
128: */
129: public void transform(Object object, StreamResult result)
130: throws TransformerException {
131:
132: Task t = createTransformTask(object, result);
133: t.run();
134: if (t.checkError()) {
135: Exception e = t.getError();
136: if (!TransformerException.class.isAssignableFrom(e
137: .getClass())) {
138: e = new TransformerException("Translator error", e);
139: }
140: throw (TransformerException) e;
141: }
142: }
143:
144: /** Create a Transformation task. This is a Runnable task
145: * which supports aborting any processing. It will not start until the
146: * run method is called.
147: *
148: */
149: public Task createTransformTask(Object object, StreamResult result)
150: throws TransformerException {
151:
152: return new Task(object, result);
153: }
154:
155: /**
156: * Perform the XML encoding of the given object into an internal buffer and
157: * return the resulting String. Calls transform(Object,Writer). <em>It
158: * should be noted the most efficient mechanism of encoding is using the
159: * OutputStream or Writer methods</em>
160: */
161: public String transform(Object object) throws TransformerException {
162: StringWriter sw = new StringWriter();
163: transform(object, sw);
164: return sw.getBuffer().toString();
165: }
166:
167: /**
168: * Create an XMLReader to use in the transformation.
169: */
170: public XMLReaderSupport createXMLReader(Object object) {
171: return new XMLReaderSupport(this , object);
172: }
173:
174: /**
175: * Get the number of spaces to indent the output xml. Defaults to -1.
176: *
177: * @return The number of spaces to indent, or -1, to disable.
178: */
179: public int getIndentation() {
180: return indentation;
181: }
182:
183: /**
184: * Set the number of spaces to indent the output xml. Default to -1.
185: *
186: * @param amt The number of spaces to indent if > 0, otherwise disable.
187: */
188: public void setIndentation(int amt) {
189: indentation = amt;
190: }
191:
192: /**
193: * Gets the charset to declare in the header of the response.
194: * @return the charset to encode with.
195: */
196: public Charset getEncoding() {
197: return charset;
198: }
199:
200: /**
201: * Sets the charset to declare in the xml header returned.
202: *
203: * @param charset A charset object of the desired encoding
204: */
205: public void setEncoding(Charset charset) {
206: this .charset = charset;
207: }
208:
209: /**
210: * Will this transformation omit the standard XML declaration. <b>Defaults
211: * to false</b>
212: *
213: * @return true if the XML declaration will be omitted, false otherwise.
214: */
215: public boolean isOmitXMLDeclaration() {
216: return xmlDecl;
217: }
218:
219: /**
220: * Set this transformer to omit/include the XML declaration. <b>Defaults to
221: * false</b>
222: *
223: * @param xmlDecl Omit/include the XML declaration.
224: */
225: public void setOmitXMLDeclaration(boolean xmlDecl) {
226: this .xmlDecl = xmlDecl;
227: }
228:
229: /**
230: * Should this transformer declare namespace prefixes in the first element
231: * it outputs? Defaults to true.
232: *
233: * @return true if namespaces will be declared, false otherwise
234: */
235: public boolean isNamespaceDeclartionEnabled() {
236: return nsDecl;
237: }
238:
239: /**
240: * Enable declaration of namespace prefixes in the first element. Defaults
241: * to true;
242: *
243: * @param enabled Enable namespace declaration.
244: */
245: public void setNamespaceDeclarationEnabled(boolean enabled) {
246: nsDecl = enabled;
247: }
248:
249: /** A wrapper for a Transformation Task. Support aborting any translation
250: * activity. Because the Task is Runnable, exceptions must be checked
251: * asynchronously by using the checkError and getError methods.
252: */
253: public class Task implements Runnable {
254:
255: //private final Translator translator;
256: private final Transformer transformer;
257: private final Source xmlSource;
258: private final StreamResult result;
259: private final XMLReaderSupport reader;
260: private Exception error;
261:
262: public Task(Object object, StreamResult result)
263: throws TransformerException {
264: transformer = createTransformer();
265: reader = createXMLReader(object);
266:
267: xmlSource = new SAXSource(createXMLReader(object),
268: new InputSource());
269:
270: this .result = result;
271: }
272:
273: /** Did an error occur?
274: * @return true if one did, false otherwise.
275: */
276: public boolean checkError() {
277: return error != null;
278: }
279:
280: /** Get any error which occurred.
281: * @return An Exception if checkError returns true, null otherwise.
282: */
283: public Exception getError() {
284: return error;
285: }
286:
287: /** Calls to the underlying translator to abort any calls to translation.
288: * Should return silently regardless of outcome.
289: */
290: public void abort() {
291: Translator t = reader.getTranslator();
292: if (t != null)
293: t.abort();
294: }
295:
296: /**
297: * Perform the translation. Exceptions are captured and can be obtained
298: * through the checkError and getError methods.
299: */
300: public void run() {
301: try {
302: transformer.transform(xmlSource, result);
303: } catch (Exception re) {
304: error = re;
305: }
306: }
307:
308: }
309:
310: /**
311: * Filter output from a ContentHandler and insert Namespace declarations in
312: * the first element.
313: */
314: private static class ContentHandlerFilter implements
315: ContentHandler, LexicalHandler {
316: private final ContentHandler original;
317: private AttributesImpl namespaceDecls;
318:
319: public ContentHandlerFilter(ContentHandler original,
320: AttributesImpl nsDecls) {
321: this .original = original;
322: this .namespaceDecls = nsDecls;
323: }
324:
325: public void characters(char[] ch, int start, int length)
326: throws SAXException {
327: original.characters(ch, start, length);
328: }
329:
330: public void endDocument() throws SAXException {
331: original.endDocument();
332: }
333:
334: public void endElement(String namespaceURI, String localName,
335: String qName) throws SAXException {
336: original.endElement(namespaceURI, localName, qName);
337: }
338:
339: public void endPrefixMapping(String prefix) throws SAXException {
340: original.endPrefixMapping(prefix);
341: }
342:
343: public void ignorableWhitespace(char[] ch, int start, int length)
344: throws SAXException {
345: original.ignorableWhitespace(ch, start, length);
346: }
347:
348: public void processingInstruction(String target, String data)
349: throws SAXException {
350: original.processingInstruction(target, data);
351: }
352:
353: public void setDocumentLocator(org.xml.sax.Locator locator) {
354: original.setDocumentLocator(locator);
355: }
356:
357: public void skippedEntity(String name) throws SAXException {
358: original.skippedEntity(name);
359: }
360:
361: public void startDocument() throws SAXException {
362: original.startDocument();
363: }
364:
365: public void startElement(String namespaceURI, String localName,
366: String qName, Attributes atts) throws SAXException {
367: if (namespaceDecls != null) {
368: for (int i = 0, ii = atts.getLength(); i < ii; i++) {
369: namespaceDecls.addAttribute(null, null, atts
370: .getQName(i), atts.getType(i), atts
371: .getValue(i));
372: }
373:
374: atts = namespaceDecls;
375: namespaceDecls = null;
376: }
377: if (namespaceURI == null)
378: namespaceURI = "";
379: if (localName == null)
380: localName = "";
381: original.startElement(namespaceURI, localName, qName, atts);
382: }
383:
384: public void startPrefixMapping(String prefix, String uri)
385: throws SAXException {
386: original.startPrefixMapping(prefix, uri);
387: }
388:
389: public void comment(char[] ch, int start, int length)
390: throws SAXException {
391: if (original instanceof LexicalHandler) {
392: ((LexicalHandler) original).comment(ch, start, length);
393: }
394: }
395:
396: public void startCDATA() throws SAXException {
397: if (original instanceof LexicalHandler) {
398: ((LexicalHandler) original).startCDATA();
399: }
400: }
401:
402: public void endCDATA() throws SAXException {
403: if (original instanceof LexicalHandler) {
404: ((LexicalHandler) original).endCDATA();
405: }
406: }
407:
408: public void startDTD(String name, String publicId,
409: String systemId) throws SAXException {
410: if (original instanceof LexicalHandler) {
411: ((LexicalHandler) original).startDTD(name, publicId,
412: systemId);
413: }
414: }
415:
416: public void endDTD() throws SAXException {
417: if (original instanceof LexicalHandler) {
418: ((LexicalHandler) original).endDTD();
419: }
420: }
421:
422: public void startEntity(String name) throws SAXException {
423: if (original instanceof LexicalHandler) {
424: ((LexicalHandler) original).startEntity(name);
425: }
426: }
427:
428: public void endEntity(String name) throws SAXException {
429: if (original instanceof LexicalHandler) {
430: ((LexicalHandler) original).endEntity(name);
431: }
432: }
433: }
434:
435: /**
436: * Support for writing Translators.
437: */
438: protected abstract static class TranslatorSupport implements
439: Translator {
440: protected final ContentHandler contentHandler;
441: private String prefix;
442: private String namespace;
443: protected final Attributes NULL_ATTS = new AttributesImpl();
444: protected NamespaceSupport nsSupport = new NamespaceSupport();
445: protected SchemaLocationSupport schemaLocation;
446: /**
447: * Subclasses should check this flag in case an abort message was sent
448: * and stop any internal iteration if false.
449: */
450: protected volatile boolean running = true;
451:
452: public TranslatorSupport(ContentHandler contentHandler,
453: String prefix, String nsURI) {
454: this .contentHandler = contentHandler;
455: this .prefix = prefix;
456: this .namespace = nsURI;
457: if (prefix != null && nsURI != null)
458: nsSupport.declarePrefix(prefix, nsURI);
459: }
460:
461: public TranslatorSupport(ContentHandler contentHandler,
462: String prefix, String nsURI,
463: SchemaLocationSupport schemaLocation) {
464: this (contentHandler, prefix, nsURI);
465: this .schemaLocation = schemaLocation;
466: }
467:
468: public void abort() {
469: running = false;
470: }
471:
472: /**
473: * Utility method to copy namespace declarations from "sub" translators
474: * into this ns support...
475: */
476: protected void addNamespaceDeclarations(TranslatorSupport trans) {
477: NamespaceSupport additional = trans.getNamespaceSupport();
478: java.util.Enumeration declared = additional
479: .getDeclaredPrefixes();
480: while (declared.hasMoreElements()) {
481: String prefix1 = declared.nextElement().toString();
482: nsSupport.declarePrefix(prefix1, additional
483: .getURI(prefix1));
484: }
485: }
486:
487: /**
488: * Utility method for creating attributes from an array of name value
489: * pairs.
490: * <p>
491: * The <tt>nameValuePairs</tt> array should be of the form:
492: * <pre>{name1,value1,name2,value2,...,nameN,valueN}</pre>
493: * </p>
494: * @param nameValuePairs The attribute names/values.
495: *
496: */
497: protected AttributesImpl createAttributes(
498: String[] nameValuePairs) {
499: AttributesImpl attributes = new AttributesImpl();
500:
501: for (int i = 0; i < nameValuePairs.length; i += 2) {
502: String name = nameValuePairs[i];
503: String value = nameValuePairs[i + 1];
504:
505: attributes.addAttribute("", name, name, "", value);
506: }
507:
508: return attributes;
509: }
510:
511: protected void element(String element, String content) {
512: element(element, content, NULL_ATTS);
513: }
514:
515: protected void element(String element, String content,
516: Attributes atts) {
517: start(element, atts);
518:
519: if (content != null) {
520: chars(content);
521: }
522:
523: end(element);
524: }
525:
526: protected void start(String element) {
527: start(element, NULL_ATTS);
528: }
529:
530: protected void start(String element, Attributes atts) {
531: try {
532: String el = (prefix == null) ? element
533: : (prefix + ":" + element);
534: contentHandler.startElement("", "", el, atts);
535: } catch (SAXException se) {
536: throw new RuntimeException(se);
537: }
538: }
539:
540: protected void chars(String text) {
541: try {
542: char[] ch = text.toCharArray();
543: contentHandler.characters(ch, 0, ch.length);
544: } catch (SAXException se) {
545: throw new RuntimeException(se);
546: }
547: }
548:
549: protected void end(String element) {
550: try {
551: String el = (prefix == null) ? element
552: : (prefix + ":" + element);
553: contentHandler.endElement("", "", el);
554: } catch (SAXException se) {
555: throw new RuntimeException(se);
556: }
557: }
558:
559: protected void cdata(String cdata) {
560: if (contentHandler instanceof LexicalHandler) {
561: LexicalHandler lexicalHandler = (LexicalHandler) contentHandler;
562: try {
563: lexicalHandler.startCDATA();
564: chars(cdata);
565: lexicalHandler.endCDATA();
566: } catch (SAXException e) {
567: throw new RuntimeException(e);
568: }
569: }
570: }
571:
572: public String getDefaultNamespace() {
573: return namespace;
574: }
575:
576: public String getDefaultPrefix() {
577: return prefix;
578: }
579:
580: public NamespaceSupport getNamespaceSupport() {
581: return nsSupport;
582: }
583:
584: public SchemaLocationSupport getSchemaLocationSupport() {
585: return schemaLocation;
586: }
587: }
588:
589: /**
590: * Adds support for schemaLocations.
591: *
592: * @task REVISIT: consider combining this with NamespaceSupport, as it
593: * would make sense to just add a location to your nsURI. Or at
594: * least tie them more closely, so that you can only add a
595: * SchemaLocation if the namespace actually exists.
596: */
597: public static class SchemaLocationSupport {
598: private Map locations = new HashMap();
599:
600: public void setLocation(String nsURI, String uri) {
601: locations.put(nsURI, uri);
602: }
603:
604: public String getSchemaLocation() {
605: return getSchemaLocation(locations.keySet());
606: }
607:
608: public String getSchemaLocation(String nsURI) {
609: String uri = (String) locations.get(nsURI);
610:
611: if (uri == null) {
612: return "";
613: }
614: return nsURI + " " + uri;
615: }
616:
617: public String getSchemaLocation(Set namespaces) {
618: StringBuffer location = new StringBuffer();
619:
620: for (Iterator it = namespaces.iterator(); it.hasNext();) {
621: location.append(getSchemaLocation((String) it.next()));
622:
623: if (it.hasNext()) {
624: location.append(" "); //\n? Pretty printing?
625: }
626: }
627:
628: return location.toString();
629: }
630: }
631:
632: /**
633: * Support for the setup of an XMLReader for use in a transformation.
634: */
635: protected static class XMLReaderSupport extends XMLFilterImpl {
636: TransformerBase base;
637: Object object;
638: Translator translator;
639:
640: public XMLReaderSupport(TransformerBase transfomerBase,
641: Object object) {
642: this .base = transfomerBase;
643: this .object = object;
644: }
645:
646: public final Translator getTranslator() {
647: return translator;
648: }
649:
650: public void parse(InputSource in) throws SAXException {
651: ContentHandler handler = getContentHandler();
652:
653: if (base.isNamespaceDeclartionEnabled()) {
654: AttributesImpl atts = new AttributesImpl();
655: ContentHandlerFilter filter = new ContentHandlerFilter(
656: handler, atts);
657: translator = base.createTranslator(filter);
658:
659: if (translator.getDefaultNamespace() != null) {
660: //declare the default mapping
661: atts.addAttribute(XMLNS_NAMESPACE, null, "xmlns",
662: "CDATA", translator.getDefaultNamespace());
663:
664: //if prefix non-null, declare the mapping
665: if (translator.getDefaultPrefix() != null) {
666: atts
667: .addAttribute(
668: XMLNS_NAMESPACE,
669: null,
670: "xmlns:"
671: + translator
672: .getDefaultPrefix(),
673: "CDATA", translator
674: .getDefaultNamespace());
675: }
676: }
677:
678: NamespaceSupport ns = translator.getNamespaceSupport();
679: java.util.Enumeration e = ns.getPrefixes();
680:
681: //TODO: only add schema locations for namespaces that are
682: //actually here, or some sort of better checking.
683: //Set namespaces = new HashSet();
684: while (e.hasMoreElements()) {
685: String prefix = e.nextElement().toString();
686:
687: if (prefix.equals("xml")) {
688: continue;
689: }
690:
691: String xmlns = "xmlns:" + prefix;
692:
693: if (atts.getValue(xmlns) == null) {
694: atts.addAttribute(XMLNS_NAMESPACE, null, xmlns,
695: "CDATA", ns.getURI(prefix));
696:
697: //namespaces.add(ns.getURI(prefix));
698: }
699: }
700:
701: String defaultNS = ns.getURI("");
702:
703: if ((defaultNS != null)
704: && (atts.getValue("xmlns:") == null)) {
705: atts.addAttribute(XMLNS_NAMESPACE, null, "xmlns:",
706: "CDATA", defaultNS);
707:
708: //namespaces.add(defaultNS);
709: }
710:
711: SchemaLocationSupport schemaLocSup = translator
712: .getSchemaLocationSupport();
713:
714: if ((schemaLocSup != null)
715: && !schemaLocSup.getSchemaLocation().equals("")) {
716: atts
717: .addAttribute(XMLNS_NAMESPACE, null,
718: "xmlns:xsi", "CDATA",
719: "http://www.w3.org/2001/XMLSchema-instance");
720: atts.addAttribute(null, null, "xsi:schemaLocation",
721: null, schemaLocSup.getSchemaLocation());
722: }
723: } else {
724: translator = base.createTranslator(handler);
725: }
726:
727: handler.startDocument();
728: translator.encode(object);
729: handler.endDocument();
730: }
731: }
732: }
|