blob: 8864aa9bc3488b4e009f57e9169dd1e1df09600d [file] [log] [blame]
/*
* Copyright 1999-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.serialization;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.cocoon.Constants;
import org.apache.cocoon.caching.CacheableProcessingComponent;
import org.apache.cocoon.core.Core;
import org.apache.cocoon.util.ClassUtils;
import org.apache.cocoon.util.TraxErrorHandler;
import org.apache.cocoon.xml.AbstractXMLPipe;
import org.apache.cocoon.xml.XMLConsumer;
import org.apache.cocoon.xml.XMLUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.excalibur.source.SourceValidity;
import org.apache.excalibur.source.impl.validity.NOPValidity;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @version $Id$
*/
public abstract class AbstractTextSerializer extends AbstractSerializer
implements Configurable, CacheableProcessingComponent {
/**
* Cache for avoiding unnecessary checks of namespaces abilities.
* It associates a Boolean to the transformer class name.
*/
private static final Map needsNamespaceCache = new HashMap();
/**
* The trax <code>TransformerFactory</code> used by this serializer.
*/
private SAXTransformerFactory tfactory;
/**
* The <code>Properties</code> used by this serializer.
*/
protected final Properties format = new Properties();
/**
* The pipe that adds namespaces as xmlns attributes.
*/
private NamespaceAsAttributes namespacePipe;
/**
* The caching key
*/
private String cachingKey = "1";
/**
* Interpose namespace pipe if needed.
*/
public void setConsumer(XMLConsumer consumer) {
if (this.namespacePipe == null) {
super.setConsumer(consumer);
} else {
this.namespacePipe.setConsumer(consumer);
super.setConsumer(this.namespacePipe);
}
}
/**
* Interpose namespace pipe if needed.
*/
public void setContentHandler(ContentHandler handler) {
if (this.namespacePipe == null) {
super.setContentHandler(handler);
} else {
this.namespacePipe.setContentHandler(handler);
super.setContentHandler(this.namespacePipe);
}
}
/**
* Interpose namespace pipe if needed.
*/
public void setLexicalHandler(LexicalHandler handler) {
if (this.namespacePipe == null) {
super.setLexicalHandler(handler);
} else {
this.namespacePipe.setLexicalHandler(handler);
super.setLexicalHandler(this.namespacePipe);
}
}
/**
* Helper for TransformerFactory.
*/
protected SAXTransformerFactory getTransformerFactory() {
return tfactory;
}
/**
* Helper for TransformerHandler.
*/
protected TransformerHandler getTransformerHandler() throws TransformerException {
return this.getTransformerFactory().newTransformerHandler();
}
/**
* Set the {@link OutputStream} where the requested resource should
* be serialized.
*/
public void setOutputStream(OutputStream out) throws IOException {
/*
* Add a level of buffering to the output stream. Xalan serializes
* every character individually. In conjunction with chunked
* transfer encoding this would otherwise lead to a whopping 6-fold
* increase of data on the wire.
*/
// if (outputBufferSize > 0) {
// super.setOutputStream(
// new BufferedOutputStream(out, outputBufferSize));
// } else {
super.setOutputStream(out);
// }
}
public void configure(Core core) {
String defaultEncoding = core.getSettings().getFormEncoding();
if (defaultEncoding != null) {
this.format.setProperty(OutputKeys.ENCODING, defaultEncoding);
}
}
/**
* Set the configurations for this serializer.
*/
public void configure(Configuration conf) throws ConfigurationException {
// configure buffer size
// Configuration bsc = conf.getChild("buffer-size", false);
// if(null != bsc)
// outputBufferSize = bsc.getValueAsInteger(DEFAULT_BUFFER_SIZE);
// configure xalan
String cdataSectionElements = conf.getChild("cdata-section-elements").getValue(null);
String dtPublic = conf.getChild("doctype-public").getValue(null);
String dtSystem = conf.getChild("doctype-system").getValue(null);
String encoding = conf.getChild("encoding").getValue(null);
String indent = conf.getChild("indent").getValue(null);
String mediaType = conf.getChild("media-type").getValue(null);
String method = conf.getChild("method").getValue(null);
String omitXMLDeclaration = conf.getChild("omit-xml-declaration").getValue(null);
String standAlone = conf.getChild("standalone").getValue(null);
String version = conf.getChild("version").getValue(null);
final StringBuffer buffer = new StringBuffer();
if (cdataSectionElements != null) {
format.put(OutputKeys.CDATA_SECTION_ELEMENTS, cdataSectionElements);
buffer.append(";cdata-section-elements=").append(cdataSectionElements);
}
if (dtPublic != null) {
format.put(OutputKeys.DOCTYPE_PUBLIC, dtPublic);
buffer.append(";doctype-public=").append(dtPublic);
}
if (dtSystem != null) {
format.put(OutputKeys.DOCTYPE_SYSTEM, dtSystem);
buffer.append(";doctype-system=").append(dtSystem);
}
if (encoding != null) {
format.put(OutputKeys.ENCODING, encoding);
buffer.append(";encoding=").append(encoding);
}
if (indent != null) {
format.put(OutputKeys.INDENT, indent);
buffer.append(";indent=").append(indent);
}
if (mediaType != null) {
format.put(OutputKeys.MEDIA_TYPE, mediaType);
buffer.append(";media-type=").append(mediaType);
}
if (method != null) {
format.put(OutputKeys.METHOD, method);
buffer.append(";method=").append(method);
}
if (omitXMLDeclaration != null) {
format.put(OutputKeys.OMIT_XML_DECLARATION, omitXMLDeclaration);
buffer.append(";omit-xml-declaration=").append(omitXMLDeclaration);
}
if (standAlone != null) {
format.put(OutputKeys.STANDALONE, standAlone);
buffer.append(";standalone=").append(standAlone);
}
if (version != null) {
format.put(OutputKeys.VERSION, version);
buffer.append(";version=").append(version);
}
if ( buffer.length() > 0 ) {
this.cachingKey = buffer.toString();
}
String tFactoryClass = conf.getChild("transformer-factory").getValue(null);
if (tFactoryClass != null) {
try {
this.tfactory = (SAXTransformerFactory) ClassUtils.newInstance(tFactoryClass);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Using transformer factory " + tFactoryClass);
}
} catch (Exception e) {
throw new ConfigurationException("Cannot load transformer factory " + tFactoryClass, e);
}
} else {
// Standard TrAX behaviour
this.tfactory = (SAXTransformerFactory) TransformerFactory.newInstance();
}
tfactory.setErrorListener(new TraxErrorHandler(getLogger()));
// Check if we need namespace as attributes.
try {
if (needsNamespacesAsAttributes()) {
// Setup a correction pipe
this.namespacePipe = new NamespaceAsAttributes();
this.namespacePipe.enableLogging(getLogger());
}
} catch (Exception e) {
getLogger().warn("Cannot know if transformer needs namespaces attributes - assuming NO.", e);
}
}
/**
* @see org.apache.avalon.excalibur.pool.Recyclable#recycle()
*/
public void recycle() {
super.recycle();
if (this.namespacePipe != null) {
this.namespacePipe.recycle();
}
}
/**
* Generate the unique key.
* This key must be unique inside the space of this component.
* This method must be invoked before the generateValidity() method.
*
* @return The generated key or <code>0</code> if the component
* is currently not cacheable.
*/
public java.io.Serializable getKey() {
return this.cachingKey;
}
/**
* Generate the validity object.
* Before this method can be invoked the generateKey() method
* must be invoked.
*
* @return The generated validity object or <code>null</code> if the
* component is currently not cacheable.
*/
public SourceValidity getValidity() {
return NOPValidity.SHARED_INSTANCE;
}
/**
* Checks if the used Trax implementation correctly handles namespaces set using
* <code>startPrefixMapping()</code>, but wants them also as 'xmlns:' attributes.
* <p>
* The check consists in sending SAX events representing a minimal namespaced document
* with namespaces defined only with calls to <code>startPrefixMapping</code> (no
* xmlns:xxx attributes) and check if they are present in the resulting text.
*/
protected boolean needsNamespacesAsAttributes() throws Exception {
SAXTransformerFactory factory = getTransformerFactory();
Boolean cacheValue = (Boolean) needsNamespaceCache.get(factory.getClass().getName());
if (cacheValue != null) {
return cacheValue.booleanValue();
} else {
// Serialize a minimal document to check how namespaces are handled.
StringWriter writer = new StringWriter();
String uri = "namespaceuri";
String prefix = "nsp";
String check = "xmlns:" + prefix + "='" + uri + "'";
TransformerHandler handler = this.getTransformerHandler();
handler.getTransformer().setOutputProperties(format);
handler.setResult(new StreamResult(writer));
// Output a single element
handler.startDocument();
handler.startPrefixMapping(prefix, uri);
handler.startElement(uri, "element", "", XMLUtils.EMPTY_ATTRIBUTES);
handler.endPrefixMapping(prefix);
handler.endDocument();
String text = writer.toString();
// Check if the namespace is there (replace " by ' to be sure of what we search in)
boolean needsIt = (text.replace('"', '\'').indexOf(check) == -1);
String msg = needsIt ? " needs namespace attributes (will be slower)." : " handles correctly namespaces.";
getLogger().debug("Trax handler " + handler.getClass().getName() + msg);
needsNamespaceCache.put(factory.getClass().getName(), BooleanUtils.toBooleanObject(needsIt));
return needsIt;
}
}
//--------------------------------------------------------------------------------------------
/**
* A pipe that ensures that all namespace prefixes are also present as
* 'xmlns:' attributes. This used to circumvent Xalan's serialization behaviour
* which is to ignore namespaces if they're not present as 'xmlns:xxx' attributes.
*/
public static class NamespaceAsAttributes extends AbstractXMLPipe {
/**
* The prefixes of startPrefixMapping() declarations for the coming element.
*/
private List prefixList = new ArrayList();
/**
* The URIs of startPrefixMapping() declarations for the coming element.
*/
private List uriList = new ArrayList();
/**
* Maps of URI<->prefix mappings. Used to work around a bug in the Xalan
* serializer.
*/
private Map uriToPrefixMap = new HashMap();
private Map prefixToUriMap = new HashMap();
/**
* True if there has been some startPrefixMapping() for the coming element.
*/
private boolean hasMappings = false;
public void startDocument() throws SAXException {
// Cleanup
this.uriToPrefixMap.clear();
this.prefixToUriMap.clear();
clearMappings();
super.startDocument();
}
/**
* Track mappings to be able to add <code>xmlns:</code> attributes
* in <code>startElement()</code>.
*/
public void startPrefixMapping(String prefix, String uri) throws SAXException {
// Store the mappings to reconstitute xmlns:attributes
// except prefixes starting with "xml": these are reserved
// VG: (uri != null) fixes NPE in startElement
if (uri != null && !prefix.startsWith("xml")) {
this.hasMappings = true;
this.prefixList.add(prefix);
this.uriList.add(uri);
// append the prefix colon now, in order to save concatenations later, but
// only for non-empty prefixes.
if (prefix.length() > 0) {
this.uriToPrefixMap.put(uri, prefix + ":");
} else {
this.uriToPrefixMap.put(uri, prefix);
}
this.prefixToUriMap.put(prefix, uri);
}
super.startPrefixMapping(prefix, uri);
}
/**
* Ensure all namespace declarations are present as <code>xmlns:</code> attributes
* and add those needed before calling superclass. This is a workaround for a Xalan bug
* (at least in version 2.0.1) : <code>org.apache.xalan.serialize.SerializerToXML</code>
* ignores <code>start/endPrefixMapping()</code>.
*/
public void startElement(String eltUri, String eltLocalName, String eltQName, Attributes attrs)
throws SAXException {
// try to restore the qName. The map already contains the colon
if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) {
eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName;
}
if (this.hasMappings) {
// Add xmlns* attributes where needed
// New Attributes if we have to add some.
AttributesImpl newAttrs = null;
int mappingCount = this.prefixList.size();
int attrCount = attrs.getLength();
for (int mapping = 0; mapping < mappingCount; mapping++) {
// Build infos for this namespace
String uri = (String) this.uriList.get(mapping);
String prefix = (String) this.prefixList.get(mapping);
String qName = prefix.equals("") ? "xmlns" : ("xmlns:" + prefix);
// Search for the corresponding xmlns* attribute
boolean found = false;
for (int attr = 0; attr < attrCount; attr++) {
if (qName.equals(attrs.getQName(attr))) {
// Check if mapping and attribute URI match
if (!uri.equals(attrs.getValue(attr))) {
getLogger().error("URI in prefix mapping and attribute do not match : '"
+ uri + "' - '" + attrs.getURI(attr) + "'");
throw new SAXException("URI in prefix mapping and attribute do not match");
}
found = true;
break;
}
}
if (!found) {
// Need to add this namespace
if (newAttrs == null) {
// Need to test if attrs is empty or we go into an infinite loop...
// Well know SAX bug which I spent 3 hours to remind of :-(
if (attrCount == 0) {
newAttrs = new AttributesImpl();
} else {
newAttrs = new AttributesImpl(attrs);
}
}
if (prefix.equals("")) {
newAttrs.addAttribute(Constants.XML_NAMESPACE_URI, "xmlns", "xmlns", "CDATA", uri);
} else {
newAttrs.addAttribute(Constants.XML_NAMESPACE_URI, prefix, qName, "CDATA", uri);
}
}
} // end for mapping
// Cleanup for the next element
clearMappings();
// Start element with new attributes, if any
super.startElement(eltUri, eltLocalName, eltQName, newAttrs == null ? attrs : newAttrs);
} else {
// Normal job
super.startElement(eltUri, eltLocalName, eltQName, attrs);
}
}
/**
* Receive notification of the end of an element.
* Try to restore the element qName.
*/
public void endElement(String eltUri, String eltLocalName, String eltQName) throws SAXException {
// try to restore the qName. The map already contains the colon
if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) {
eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName;
}
super.endElement(eltUri, eltLocalName, eltQName);
}
/**
* End the scope of a prefix-URI mapping:
* remove entry from mapping tables.
*/
public void endPrefixMapping(String prefix) throws SAXException {
// remove mappings for xalan-bug-workaround.
// Unfortunately, we're not passed the uri, but the prefix here,
// so we need to maintain maps in both directions.
if (this.prefixToUriMap.containsKey(prefix)) {
this.uriToPrefixMap.remove(this.prefixToUriMap.get(prefix));
this.prefixToUriMap.remove(prefix);
}
if (hasMappings) {
// most of the time, start/endPrefixMapping calls have an element event between them,
// which will clear the hasMapping flag and so this code will only be executed in the
// rather rare occasion when there are start/endPrefixMapping calls with no element
// event in between. If we wouldn't remove the items from the prefixList and uriList here,
// the namespace would be incorrectly declared on the next element following the
// endPrefixMapping call.
int pos = prefixList.lastIndexOf(prefix);
if (pos != -1) {
prefixList.remove(pos);
uriList.remove(pos);
}
}
super.endPrefixMapping(prefix);
}
/**
*
*/
public void endDocument() throws SAXException {
// Cleanup
this.uriToPrefixMap.clear();
this.prefixToUriMap.clear();
clearMappings();
super.endDocument();
}
private void clearMappings() {
this.hasMappings = false;
this.prefixList.clear();
this.uriList.clear();
}
}
/* (non-Javadoc)
* @see org.xml.sax.ContentHandler#endDocument()
*/
public void endDocument() throws SAXException {
super.endDocument();
// if (this.output != null) {
// try {
// this.output.flush();
// } catch (IOException ignored) {
// }
// }
}
}