001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.serialization;
018:
019: import org.apache.avalon.framework.configuration.Configurable;
020: import org.apache.avalon.framework.configuration.Configuration;
021: import org.apache.avalon.framework.configuration.ConfigurationException;
022: import org.apache.avalon.framework.context.Context;
023: import org.apache.avalon.framework.context.ContextException;
024: import org.apache.avalon.framework.context.Contextualizable;
025: import org.apache.cocoon.Constants;
026: import org.apache.cocoon.caching.CacheableProcessingComponent;
027: import org.apache.cocoon.util.ClassUtils;
028: import org.apache.cocoon.util.TraxErrorHandler;
029: import org.apache.cocoon.xml.AbstractXMLPipe;
030: import org.apache.cocoon.xml.XMLConsumer;
031: import org.apache.cocoon.xml.XMLUtils;
032:
033: import org.apache.commons.lang.BooleanUtils;
034: import org.apache.excalibur.source.SourceValidity;
035: import org.apache.excalibur.source.impl.validity.NOPValidity;
036: import org.xml.sax.Attributes;
037: import org.xml.sax.ContentHandler;
038: import org.xml.sax.SAXException;
039: import org.xml.sax.ext.LexicalHandler;
040: import org.xml.sax.helpers.AttributesImpl;
041:
042: import javax.xml.transform.OutputKeys;
043: import javax.xml.transform.TransformerFactory;
044: import javax.xml.transform.TransformerException;
045: import javax.xml.transform.sax.SAXTransformerFactory;
046: import javax.xml.transform.sax.TransformerHandler;
047: import javax.xml.transform.stream.StreamResult;
048: import java.io.StringWriter;
049: import java.util.ArrayList;
050: import java.util.HashMap;
051: import java.util.List;
052: import java.util.Map;
053: import java.util.Properties;
054:
055: /**
056: * @author <a href="mailto:pier@apache.org">Pierpaolo Fumagalli</a>
057: * (Apache Software Foundation)
058: * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
059: * @author <a href="mailto:sylvain.wallez@anyware-tech.com">Sylvain Wallez</a>
060: * @version $Id: AbstractTextSerializer.java 433543 2006-08-22 06:22:54Z crossley $
061: */
062: public abstract class AbstractTextSerializer extends AbstractSerializer
063: implements Configurable, CacheableProcessingComponent,
064: Contextualizable {
065:
066: /**
067: * Cache for avoiding unnecessary checks of namespaces abilities.
068: * It associates a Boolean to the transformer class name.
069: */
070: private static final Map needsNamespaceCache = new HashMap();
071:
072: /**
073: * The trax <code>TransformerFactory</code> used by this serializer.
074: */
075: private SAXTransformerFactory tfactory;
076:
077: /**
078: * The <code>Properties</code> used by this serializer.
079: */
080: protected final Properties format = new Properties();
081:
082: /**
083: * The pipe that adds namespaces as xmlns attributes.
084: */
085: private NamespaceAsAttributes namespacePipe;
086:
087: /**
088: * The caching key
089: */
090: private String cachingKey = "1";
091:
092: /**
093: * Interpose namespace pipe if needed.
094: */
095: public void setConsumer(XMLConsumer consumer) {
096: if (this .namespacePipe == null) {
097: super .setConsumer(consumer);
098: } else {
099: this .namespacePipe.setConsumer(consumer);
100: super .setConsumer(this .namespacePipe);
101: }
102: }
103:
104: /**
105: * Interpose namespace pipe if needed.
106: */
107: public void setContentHandler(ContentHandler handler) {
108: if (this .namespacePipe == null) {
109: super .setContentHandler(handler);
110: } else {
111: this .namespacePipe.setContentHandler(handler);
112: super .setContentHandler(this .namespacePipe);
113: }
114: }
115:
116: /**
117: * Interpose namespace pipe if needed.
118: */
119: public void setLexicalHandler(LexicalHandler handler) {
120: if (this .namespacePipe == null) {
121: super .setLexicalHandler(handler);
122: } else {
123: this .namespacePipe.setLexicalHandler(handler);
124: super .setLexicalHandler(this .namespacePipe);
125: }
126: }
127:
128: /**
129: * Helper for TransformerFactory.
130: */
131: protected SAXTransformerFactory getTransformerFactory() {
132: return tfactory;
133: }
134:
135: /**
136: * Helper for TransformerHandler.
137: */
138: protected TransformerHandler getTransformerHandler()
139: throws TransformerException {
140: return this .getTransformerFactory().newTransformerHandler();
141: }
142:
143: ///**
144: // * Set the {@link OutputStream} where the requested resource should
145: // * be serialized.
146: // */
147: // public void setOutputStream(OutputStream out) throws IOException {
148: /*
149: * Add a level of buffering to the output stream. Xalan serializes
150: * every character individually. In conjunction with chunked
151: * transfer encoding this would otherwise lead to a whopping 6-fold
152: * increase of data on the wire.
153: */
154: // if (outputBufferSize > 0) {
155: // super.setOutputStream(
156: // new BufferedOutputStream(out, outputBufferSize));
157: // } else {
158: // super.setOutputStream(out);
159: // }
160: // }
161: /**
162: * Uses the context to retrieve a default encoding for the serializers.
163: */
164: public void contextualize(Context context) throws ContextException {
165: String defaultEncoding = (String) context
166: .get(Constants.CONTEXT_DEFAULT_ENCODING);
167: if (defaultEncoding != null) {
168: this .format.setProperty(OutputKeys.ENCODING,
169: defaultEncoding);
170: }
171: }
172:
173: /**
174: * Set the configurations for this serializer.
175: */
176: public void configure(Configuration conf)
177: throws ConfigurationException {
178: // configure buffer size
179: // Configuration bsc = conf.getChild("buffer-size", false);
180: // if(null != bsc)
181: // outputBufferSize = bsc.getValueAsInteger(DEFAULT_BUFFER_SIZE);
182:
183: // configure xalan
184: String cdataSectionElements = conf.getChild(
185: "cdata-section-elements").getValue(null);
186: String dtPublic = conf.getChild("doctype-public")
187: .getValue(null);
188: String dtSystem = conf.getChild("doctype-system")
189: .getValue(null);
190: String encoding = conf.getChild("encoding").getValue(null);
191: String indent = conf.getChild("indent").getValue(null);
192: String mediaType = conf.getChild("media-type").getValue(null);
193: String method = conf.getChild("method").getValue(null);
194: String omitXMLDeclaration = conf.getChild(
195: "omit-xml-declaration").getValue(null);
196: String standAlone = conf.getChild("standalone").getValue(null);
197: String version = conf.getChild("version").getValue(null);
198:
199: final StringBuffer buffer = new StringBuffer();
200:
201: if (cdataSectionElements != null) {
202: format.put(OutputKeys.CDATA_SECTION_ELEMENTS,
203: cdataSectionElements);
204: buffer.append(";cdata-section-elements=").append(
205: cdataSectionElements);
206: }
207: if (dtPublic != null) {
208: format.put(OutputKeys.DOCTYPE_PUBLIC, dtPublic);
209: buffer.append(";doctype-public=").append(dtPublic);
210: }
211: if (dtSystem != null) {
212: format.put(OutputKeys.DOCTYPE_SYSTEM, dtSystem);
213: buffer.append(";doctype-system=").append(dtSystem);
214: }
215: if (encoding != null) {
216: format.put(OutputKeys.ENCODING, encoding);
217: buffer.append(";encoding=").append(encoding);
218: }
219: if (indent != null) {
220: format.put(OutputKeys.INDENT, indent);
221: buffer.append(";indent=").append(indent);
222: }
223: if (mediaType != null) {
224: format.put(OutputKeys.MEDIA_TYPE, mediaType);
225: buffer.append(";media-type=").append(mediaType);
226: }
227: if (method != null) {
228: format.put(OutputKeys.METHOD, method);
229: buffer.append(";method=").append(method);
230: }
231: if (omitXMLDeclaration != null) {
232: format.put(OutputKeys.OMIT_XML_DECLARATION,
233: omitXMLDeclaration);
234: buffer.append(";omit-xml-declaration=").append(
235: omitXMLDeclaration);
236: }
237: if (standAlone != null) {
238: format.put(OutputKeys.STANDALONE, standAlone);
239: buffer.append(";standalone=").append(standAlone);
240: }
241: if (version != null) {
242: format.put(OutputKeys.VERSION, version);
243: buffer.append(";version=").append(version);
244: }
245:
246: if (buffer.length() > 0) {
247: this .cachingKey = buffer.toString();
248: }
249:
250: String tFactoryClass = conf.getChild("transformer-factory")
251: .getValue(null);
252: if (tFactoryClass != null) {
253: try {
254: this .tfactory = (SAXTransformerFactory) ClassUtils
255: .newInstance(tFactoryClass);
256: if (getLogger().isDebugEnabled()) {
257: getLogger().debug(
258: "Using transformer factory "
259: + tFactoryClass);
260: }
261: } catch (Exception e) {
262: throw new ConfigurationException(
263: "Cannot load transformer factory "
264: + tFactoryClass, e);
265: }
266: } else {
267: // Standard TrAX behaviour
268: this .tfactory = (SAXTransformerFactory) TransformerFactory
269: .newInstance();
270: }
271: tfactory.setErrorListener(new TraxErrorHandler(getLogger()));
272:
273: // Check if we need namespace as attributes.
274: try {
275: if (needsNamespacesAsAttributes()) {
276: // Setup a correction pipe
277: this .namespacePipe = new NamespaceAsAttributes();
278: this .namespacePipe.enableLogging(getLogger());
279: }
280: } catch (Exception e) {
281: getLogger()
282: .warn(
283: "Cannot know if transformer needs namespaces attributes - assuming NO.",
284: e);
285: }
286: }
287:
288: /**
289: * @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
290: */
291: public void recycle() {
292: super .recycle();
293:
294: if (this .namespacePipe != null) {
295: this .namespacePipe.recycle();
296: }
297: }
298:
299: /**
300: * Generate the unique key.
301: * This key must be unique inside the space of this component.
302: * This method must be invoked before the generateValidity() method.
303: *
304: * @return The generated key or <code>0</code> if the component
305: * is currently not cacheable.
306: */
307: public java.io.Serializable getKey() {
308: return this .cachingKey;
309: }
310:
311: /**
312: * Generate the validity object.
313: * Before this method can be invoked the generateKey() method
314: * must be invoked.
315: *
316: * @return The generated validity object or <code>null</code> if the
317: * component is currently not cacheable.
318: */
319: public SourceValidity getValidity() {
320: return NOPValidity.SHARED_INSTANCE;
321: }
322:
323: /**
324: * Checks if the used Trax implementation correctly handles namespaces set using
325: * <code>startPrefixMapping()</code>, but wants them also as 'xmlns:' attributes.
326: * <p>
327: * The check consists in sending SAX events representing a minimal namespaced document
328: * with namespaces defined only with calls to <code>startPrefixMapping</code> (no
329: * xmlns:xxx attributes) and check if they are present in the resulting text.
330: */
331: protected boolean needsNamespacesAsAttributes() throws Exception {
332:
333: SAXTransformerFactory factory = getTransformerFactory();
334:
335: Boolean cacheValue = (Boolean) needsNamespaceCache.get(factory
336: .getClass().getName());
337: if (cacheValue != null) {
338: return cacheValue.booleanValue();
339: } else {
340: // Serialize a minimal document to check how namespaces are handled.
341: StringWriter writer = new StringWriter();
342:
343: String uri = "namespaceuri";
344: String prefix = "nsp";
345: String check = "xmlns:" + prefix + "='" + uri + "'";
346:
347: TransformerHandler handler = this .getTransformerHandler();
348:
349: handler.getTransformer().setOutputProperties(format);
350: handler.setResult(new StreamResult(writer));
351:
352: // Output a single element
353: handler.startDocument();
354: handler.startPrefixMapping(prefix, uri);
355: handler.startElement(uri, "element", "element",
356: XMLUtils.EMPTY_ATTRIBUTES);
357: handler.endElement(uri, "element", "element");
358: handler.endPrefixMapping(prefix);
359: handler.endDocument();
360:
361: String text = writer.toString();
362:
363: // Check if the namespace is there (replace " by ' to be sure of what we search in)
364: boolean needsIt = (text.replace('"', '\'').indexOf(check) == -1);
365:
366: String msg = needsIt ? " needs namespace attributes (will be slower)."
367: : " handles correctly namespaces.";
368:
369: getLogger().debug(
370: "Trax handler " + handler.getClass().getName()
371: + msg);
372:
373: needsNamespaceCache.put(factory.getClass().getName(),
374: BooleanUtils.toBooleanObject(needsIt));
375:
376: return needsIt;
377: }
378: }
379:
380: //--------------------------------------------------------------------------------------------
381:
382: /**
383: * A pipe that ensures that all namespace prefixes are also present as
384: * 'xmlns:' attributes. This used to circumvent Xalan's serialization behaviour
385: * which is to ignore namespaces if they're not present as 'xmlns:xxx' attributes.
386: */
387: public static class NamespaceAsAttributes extends AbstractXMLPipe {
388:
389: /**
390: * The prefixes of startPrefixMapping() declarations for the coming element.
391: */
392: private List prefixList = new ArrayList();
393:
394: /**
395: * The URIs of startPrefixMapping() declarations for the coming element.
396: */
397: private List uriList = new ArrayList();
398:
399: /**
400: * Maps of URI<->prefix mappings. Used to work around a bug in the Xalan
401: * serializer.
402: */
403: private Map uriToPrefixMap = new HashMap();
404: private Map prefixToUriMap = new HashMap();
405:
406: /**
407: * True if there has been some startPrefixMapping() for the coming element.
408: */
409: private boolean hasMappings = false;
410:
411: public void startDocument() throws SAXException {
412: // Cleanup
413: this .uriToPrefixMap.clear();
414: this .prefixToUriMap.clear();
415: clearMappings();
416: super .startDocument();
417: }
418:
419: /**
420: * Track mappings to be able to add <code>xmlns:</code> attributes
421: * in <code>startElement()</code>.
422: */
423: public void startPrefixMapping(String prefix, String uri)
424: throws SAXException {
425: // Store the mappings to reconstitute xmlns:attributes
426: // except prefixes starting with "xml": these are reserved
427: // VG: (uri != null) fixes NPE in startElement
428: if (uri != null && !prefix.startsWith("xml")) {
429: this .hasMappings = true;
430: this .prefixList.add(prefix);
431: this .uriList.add(uri);
432:
433: // append the prefix colon now, in order to save concatenations later, but
434: // only for non-empty prefixes.
435: if (prefix.length() > 0) {
436: this .uriToPrefixMap.put(uri, prefix + ":");
437: } else {
438: this .uriToPrefixMap.put(uri, prefix);
439: }
440:
441: this .prefixToUriMap.put(prefix, uri);
442: }
443: super .startPrefixMapping(prefix, uri);
444: }
445:
446: /**
447: * Ensure all namespace declarations are present as <code>xmlns:</code> attributes
448: * and add those needed before calling superclass. This is a workaround for a Xalan bug
449: * (at least in version 2.0.1) : <code>org.apache.xalan.serialize.SerializerToXML</code>
450: * ignores <code>start/endPrefixMapping()</code>.
451: */
452: public void startElement(String eltUri, String eltLocalName,
453: String eltQName, Attributes attrs) throws SAXException {
454:
455: // try to restore the qName. The map already contains the colon
456: if (null != eltUri && eltUri.length() != 0
457: && this .uriToPrefixMap.containsKey(eltUri)) {
458: eltQName = this .uriToPrefixMap.get(eltUri)
459: + eltLocalName;
460: }
461: if (this .hasMappings) {
462: // Add xmlns* attributes where needed
463:
464: // New Attributes if we have to add some.
465: AttributesImpl newAttrs = null;
466:
467: int mappingCount = this .prefixList.size();
468: int attrCount = attrs.getLength();
469:
470: for (int mapping = 0; mapping < mappingCount; mapping++) {
471:
472: // Build infos for this namespace
473: String uri = (String) this .uriList.get(mapping);
474: String prefix = (String) this .prefixList
475: .get(mapping);
476: String qName = prefix.length() == 0 ? "xmlns"
477: : ("xmlns:" + prefix);
478:
479: // Search for the corresponding xmlns* attribute
480: boolean found = false;
481: for (int attr = 0; attr < attrCount; attr++) {
482: if (qName.equals(attrs.getQName(attr))) {
483: // Check if mapping and attribute URI match
484: if (!uri.equals(attrs.getValue(attr))) {
485: getLogger().error(
486: "URI in prefix mapping and attribute do not match : '"
487: + uri + "' - '"
488: + attrs.getURI(attr)
489: + "'");
490: throw new SAXException(
491: "URI in prefix mapping and attribute do not match");
492: }
493: found = true;
494: break;
495: }
496: }
497:
498: if (!found) {
499: // Need to add this namespace
500: if (newAttrs == null) {
501: // Need to test if attrs is empty or we go into an infinite loop...
502: // Well know SAX bug which I spent 3 hours to remind of :-(
503: if (attrCount == 0) {
504: newAttrs = new AttributesImpl();
505: } else {
506: newAttrs = new AttributesImpl(attrs);
507: }
508: }
509:
510: if (prefix.length() == 0) {
511: newAttrs.addAttribute(
512: Constants.XML_NAMESPACE_URI,
513: "xmlns", "xmlns", "CDATA", uri);
514: } else {
515: newAttrs.addAttribute(
516: Constants.XML_NAMESPACE_URI,
517: prefix, qName, "CDATA", uri);
518: }
519: }
520: } // end for mapping
521:
522: // Cleanup for the next element
523: clearMappings();
524:
525: // Start element with new attributes, if any
526: super .startElement(eltUri, eltLocalName, eltQName,
527: newAttrs == null ? attrs : newAttrs);
528: } else {
529: // Normal job
530: super .startElement(eltUri, eltLocalName, eltQName,
531: attrs);
532: }
533: }
534:
535: /**
536: * Receive notification of the end of an element.
537: * Try to restore the element qName.
538: */
539: public void endElement(String eltUri, String eltLocalName,
540: String eltQName) throws SAXException {
541: // try to restore the qName. The map already contains the colon
542: if (null != eltUri && eltUri.length() != 0
543: && this .uriToPrefixMap.containsKey(eltUri)) {
544: eltQName = this .uriToPrefixMap.get(eltUri)
545: + eltLocalName;
546: }
547: super .endElement(eltUri, eltLocalName, eltQName);
548: }
549:
550: /**
551: * End the scope of a prefix-URI mapping:
552: * remove entry from mapping tables.
553: */
554: public void endPrefixMapping(String prefix) throws SAXException {
555: // remove mappings for xalan-bug-workaround.
556: // Unfortunately, we're not passed the uri, but the prefix here,
557: // so we need to maintain maps in both directions.
558: if (this .prefixToUriMap.containsKey(prefix)) {
559: this .uriToPrefixMap.remove(this .prefixToUriMap
560: .get(prefix));
561: this .prefixToUriMap.remove(prefix);
562: }
563:
564: if (hasMappings) {
565: // most of the time, start/endPrefixMapping calls have an element event between them,
566: // which will clear the hasMapping flag and so this code will only be executed in the
567: // rather rare occasion when there are start/endPrefixMapping calls with no element
568: // event in between. If we wouldn't remove the items from the prefixList and uriList here,
569: // the namespace would be incorrectly declared on the next element following the
570: // endPrefixMapping call.
571: int pos = prefixList.lastIndexOf(prefix);
572: if (pos != -1) {
573: prefixList.remove(pos);
574: uriList.remove(pos);
575: }
576: }
577:
578: super .endPrefixMapping(prefix);
579: }
580:
581: /**
582: *
583: */
584: public void endDocument() throws SAXException {
585: // Cleanup
586: this .uriToPrefixMap.clear();
587: this .prefixToUriMap.clear();
588: clearMappings();
589: super .endDocument();
590: }
591:
592: private void clearMappings() {
593: this .hasMappings = false;
594: this.prefixList.clear();
595: this.uriList.clear();
596: }
597: }
598: }
|