001: /*
002: * Copyright 2004 Outerthought bvba and Schaubroeck nv
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.outerj.daisy.xmlutil;
017:
018: import org.xml.sax.SAXException;
019: import org.xml.sax.Attributes;
020: import org.xml.sax.ContentHandler;
021: import org.xml.sax.Locator;
022: import org.xml.sax.helpers.AttributesImpl;
023:
024: import javax.xml.transform.sax.SAXTransformerFactory;
025: import javax.xml.transform.sax.TransformerHandler;
026: import javax.xml.transform.stream.StreamResult;
027: import javax.xml.transform.TransformerConfigurationException;
028: import javax.xml.transform.OutputKeys;
029: import java.util.*;
030: import java.io.StringWriter;
031: import java.io.OutputStream;
032:
033: public class XmlSerializer implements ContentHandler {
034: private static boolean needsNamespaceFix = false;
035: private static boolean needsNamespaceFixInitialized = false;
036: private static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
037:
038: private ContentHandler nextHandler;
039:
040: public XmlSerializer(OutputStream outputStream) throws Exception {
041: this (outputStream, null);
042: }
043:
044: public XmlSerializer(OutputStream outputStream,
045: Properties properties) throws Exception {
046: if (!needsNamespaceFixInitialized) {
047: synchronized (this ) {
048: // PS: I know that double checking does not work reliably, but
049: // that's not important here. It can't do harm if this is executed
050: // multiple times.
051: if (!needsNamespaceFixInitialized) {
052: needsNamespaceFix = needsNamespacesAsAttributes();
053: needsNamespaceFixInitialized = true;
054: }
055: }
056: }
057:
058: TransformerHandler serializer = getTransformerHandler();
059: serializer.getTransformer().setOutputProperty(
060: OutputKeys.METHOD, "xml");
061: if (properties != null)
062: serializer.getTransformer().setOutputProperties(properties);
063: serializer.setResult(new StreamResult(outputStream));
064:
065: if (needsNamespaceFix) {
066: nextHandler = new NamespaceAsAttributes(serializer);
067: } else {
068: nextHandler = serializer;
069: }
070: }
071:
072: private TransformerHandler getTransformerHandler()
073: throws TransformerConfigurationException {
074: SAXTransformerFactory transformerFactory = (SAXTransformerFactory) SAXTransformerFactory
075: .newInstance();
076: return transformerFactory.newTransformerHandler();
077: }
078:
079: /**
080: * Checks if the used Trax implementation correctly handles namespaces set using
081: * <code>startPrefixMapping()</code>, but wants them also as 'xmlns:' attributes.
082: * <p>
083: * The check consists in sending SAX events representing a minimal namespaced document
084: * with namespaces defined only with calls to <code>startPrefixMapping</code> (no
085: * xmlns:xxx attributes) and check if they are present in the resulting text.
086: *
087: * <p>THIS METHOD IS COPIED FROM COCOON'S AbstractTextSerializer with some modifications
088: */
089: private boolean needsNamespacesAsAttributes() throws Exception {
090:
091: // Serialize a minimal document to check how namespaces are handled.
092: StringWriter writer = new StringWriter();
093:
094: String uri = "namespaceuri";
095: String prefix = "nsp";
096: String check = "xmlns:" + prefix + "='" + uri + "'";
097:
098: TransformerHandler handler = getTransformerHandler();
099:
100: handler.setResult(new StreamResult(writer));
101:
102: // Output a single element
103: handler.startDocument();
104: handler.startPrefixMapping(prefix, uri);
105: handler.startElement(uri, "element", "", new AttributesImpl());
106: handler.endPrefixMapping(prefix);
107: handler.endDocument();
108:
109: String text = writer.toString();
110:
111: // Check if the namespace is there (replace " by ' to be sure of what we search in)
112: boolean needsIt = (text.replace('"', '\'').indexOf(check) == -1);
113:
114: // String msg = needsIt ? " needs namespace attributes (will be slower)." : " handles correctly namespaces.";
115: // logger.debug("Trax handler " + handler.getClass().getName() + msg);
116:
117: return needsIt;
118: }
119:
120: /**
121: * A pipe that ensures that all namespace prefixes are also present as
122: * 'xmlns:' attributes. This used to circumvent Xalan's serialization behaviour
123: * which is to ignore namespaces if they're not present as 'xmlns:xxx' attributes.
124: *
125: * <p>THIS CLASS IS COPIED FROM COCOON'S AbstractTextSerializer with some
126: * modifications
127: */
128: public class NamespaceAsAttributes implements ContentHandler {
129:
130: /**
131: * The prefixes of startPreficMapping() declarations for the coming element.
132: */
133: private List prefixList = new ArrayList();
134:
135: /**
136: * The URIs of startPrefixMapping() declarations for the coming element.
137: */
138: private List uriList = new ArrayList();
139:
140: /**
141: * Maps of URI<->prefix mappings. Used to work around a bug in the Xalan
142: * serializer.
143: */
144: private Map uriToPrefixMap = new HashMap();
145: private Map prefixToUriMap = new HashMap();
146:
147: /**
148: * True if there has been some startPrefixMapping() for the coming element.
149: */
150: private boolean hasMappings = false;
151:
152: private ContentHandler nextHandler;
153:
154: public NamespaceAsAttributes(ContentHandler nextHandler) {
155: this .nextHandler = nextHandler;
156: }
157:
158: public void startDocument() throws SAXException {
159: // Cleanup
160: this .uriToPrefixMap.clear();
161: this .prefixToUriMap.clear();
162: clearMappings();
163: nextHandler.startDocument();
164: }
165:
166: /**
167: * Track mappings to be able to add <code>xmlns:</code> attributes
168: * in <code>startElement()</code>.
169: */
170: public void startPrefixMapping(String prefix, String uri)
171: throws SAXException {
172: // Store the mappings to reconstitute xmlns:attributes
173: // except prefixes starting with "xml": these are reserved
174: // VG: (uri != null) fixes NPE in startElement
175: if (uri != null && !prefix.startsWith("xml")
176: && !this .prefixList.contains(prefix)) {
177: this .hasMappings = true;
178: this .prefixList.add(prefix);
179: this .uriList.add(uri);
180:
181: // append the prefix colon now, in order to save concatenations later, but
182: // only for non-empty prefixes.
183: if (prefix.length() > 0) {
184: this .uriToPrefixMap.put(uri, prefix + ":");
185: } else {
186: this .uriToPrefixMap.put(uri, prefix);
187: }
188:
189: this .prefixToUriMap.put(prefix, uri);
190: }
191: nextHandler.startPrefixMapping(prefix, uri);
192: }
193:
194: /**
195: * Ensure all namespace declarations are present as <code>xmlns:</code> attributes
196: * and add those needed before calling superclass. This is a workaround for a Xalan bug
197: * (at least in version 2.0.1) : <code>org.apache.xalan.serialize.SerializerToXML</code>
198: * ignores <code>start/endPrefixMapping()</code>.
199: */
200: public void startElement(String eltUri, String eltLocalName,
201: String eltQName, Attributes attrs) throws SAXException {
202:
203: // try to restore the qName. The map already contains the colon
204: if (null != eltUri && eltUri.length() != 0
205: && this .uriToPrefixMap.containsKey(eltUri)) {
206: eltQName = this .uriToPrefixMap.get(eltUri)
207: + eltLocalName;
208: }
209: if (this .hasMappings) {
210: // Add xmlns* attributes where needed
211:
212: // New Attributes if we have to add some.
213: AttributesImpl newAttrs = null;
214:
215: int mappingCount = this .prefixList.size();
216: int attrCount = attrs.getLength();
217:
218: for (int mapping = 0; mapping < mappingCount; mapping++) {
219:
220: // Build infos for this namespace
221: String uri = (String) this .uriList.get(mapping);
222: String prefix = (String) this .prefixList
223: .get(mapping);
224: String qName = prefix.equals("") ? "xmlns"
225: : ("xmlns:" + prefix);
226:
227: // Search for the corresponding xmlns* attribute
228: boolean found = false;
229: for (int attr = 0; attr < attrCount; attr++) {
230: if (qName.equals(attrs.getQName(attr))) {
231: // Check if mapping and attribute URI match
232: if (!uri.equals(attrs.getValue(attr))) {
233: System.err
234: .println("XML serializer: URI in prefix mapping and attribute do not match : '"
235: + uri
236: + "' - '"
237: + attrs.getURI(attr)
238: + "'");
239: throw new SAXException(
240: "URI in prefix mapping and attribute do not match");
241: }
242: found = true;
243: break;
244: }
245: }
246:
247: if (!found) {
248: // Need to add this namespace
249: if (newAttrs == null) {
250: // Need to test if attrs is empty or we go into an infinite loop...
251: // Well know SAX bug which I spent 3 hours to remind of :-(
252: if (attrCount == 0) {
253: newAttrs = new AttributesImpl();
254: } else {
255: newAttrs = new AttributesImpl(attrs);
256: }
257: }
258:
259: if (prefix.equals("")) {
260: newAttrs.addAttribute(XML_NAMESPACE_URI,
261: "xmlns", "xmlns", "CDATA", uri);
262: } else {
263: newAttrs.addAttribute(XML_NAMESPACE_URI,
264: prefix, qName, "CDATA", uri);
265: }
266: }
267: } // end for mapping
268:
269: // Cleanup for the next element
270: clearMappings();
271:
272: // Start element with new attributes, if any
273: nextHandler.startElement(eltUri, eltLocalName,
274: eltQName, newAttrs == null ? attrs : newAttrs);
275: } else {
276: // Normal job
277: nextHandler.startElement(eltUri, eltLocalName,
278: eltQName, attrs);
279: }
280: }
281:
282: /**
283: * Receive notification of the end of an element.
284: * Try to restore the element qName.
285: */
286: public void endElement(String eltUri, String eltLocalName,
287: String eltQName) throws SAXException {
288: // try to restore the qName. The map already contains the colon
289: if (null != eltUri && eltUri.length() != 0
290: && this .uriToPrefixMap.containsKey(eltUri)) {
291: eltQName = this .uriToPrefixMap.get(eltUri)
292: + eltLocalName;
293: }
294: nextHandler.endElement(eltUri, eltLocalName, eltQName);
295: }
296:
297: /**
298: * End the scope of a prefix-URI mapping:
299: * remove entry from mapping tables.
300: */
301: public void endPrefixMapping(String prefix) throws SAXException {
302: // remove mappings for xalan-bug-workaround.
303: // Unfortunately, we're not passed the uri, but the prefix here,
304: // so we need to maintain maps in both directions.
305: if (this .prefixToUriMap.containsKey(prefix)) {
306: this .uriToPrefixMap.remove(this .prefixToUriMap
307: .get(prefix));
308: this .prefixToUriMap.remove(prefix);
309: }
310:
311: if (hasMappings) {
312: // most of the time, start/endPrefixMapping calls have an element event between them,
313: // which will clear the hasMapping flag and so this code will only be executed in the
314: // rather rare occasion when there are start/endPrefixMapping calls with no element
315: // event in between. If we wouldn't remove the items from the prefixList and uriList here,
316: // the namespace would be incorrectly declared on the next element following the
317: // endPrefixMapping call.
318: int pos = prefixList.lastIndexOf(prefix);
319: if (pos != -1) {
320: prefixList.remove(pos);
321: uriList.remove(pos);
322: }
323: }
324:
325: nextHandler.endPrefixMapping(prefix);
326: }
327:
328: /**
329: *
330: */
331: public void endDocument() throws SAXException {
332: // Cleanup
333: this .uriToPrefixMap.clear();
334: this .prefixToUriMap.clear();
335: clearMappings();
336: nextHandler.endDocument();
337: }
338:
339: private void clearMappings() {
340: this .hasMappings = false;
341: this .prefixList.clear();
342: this .uriList.clear();
343: }
344:
345: public void characters(char ch[], int start, int length)
346: throws SAXException {
347: nextHandler.characters(ch, start, length);
348: }
349:
350: public void ignorableWhitespace(char ch[], int start, int length)
351: throws SAXException {
352: nextHandler.ignorableWhitespace(ch, start, length);
353: }
354:
355: public void skippedEntity(String name) throws SAXException {
356: nextHandler.skippedEntity(name);
357: }
358:
359: public void setDocumentLocator(Locator locator) {
360: nextHandler.setDocumentLocator(locator);
361: }
362:
363: public void processingInstruction(String target, String data)
364: throws SAXException {
365: nextHandler.processingInstruction(target, data);
366: }
367: }
368:
369: public void endDocument() throws SAXException {
370: nextHandler.endDocument();
371: }
372:
373: public void startDocument() throws SAXException {
374: nextHandler.startDocument();
375: }
376:
377: public void characters(char ch[], int start, int length)
378: throws SAXException {
379: nextHandler.characters(ch, start, length);
380: }
381:
382: public void ignorableWhitespace(char ch[], int start, int length)
383: throws SAXException {
384: nextHandler.ignorableWhitespace(ch, start, length);
385: }
386:
387: public void endPrefixMapping(String prefix) throws SAXException {
388: nextHandler.endPrefixMapping(prefix);
389: }
390:
391: public void skippedEntity(String name) throws SAXException {
392: nextHandler.skippedEntity(name);
393: }
394:
395: public void setDocumentLocator(Locator locator) {
396: nextHandler.setDocumentLocator(locator);
397: }
398:
399: public void processingInstruction(String target, String data)
400: throws SAXException {
401: nextHandler.processingInstruction(target, data);
402: }
403:
404: public void startPrefixMapping(String prefix, String uri)
405: throws SAXException {
406: nextHandler.startPrefixMapping(prefix, uri);
407: }
408:
409: public void endElement(String namespaceURI, String localName,
410: String qName) throws SAXException {
411: nextHandler.endElement(namespaceURI, localName, qName);
412: }
413:
414: public void startElement(String namespaceURI, String localName,
415: String qName, Attributes atts) throws SAXException {
416: nextHandler.startElement(namespaceURI, localName, qName, atts);
417: }
418: }
|