/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.sling.rewriter.impl.components;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import org.apache.sling.rewriter.Serializer;
import org.apache.sling.rewriter.SerializerFactory;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;
/**
* Base class for trax based serializers.
*/
public abstract class AbstractTraxSerializerFactory implements SerializerFactory {
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
/**
* The trax <code>TransformerFactory</code> used by this serializer.
*/
private SAXTransformerFactory tfactory;
private boolean needsNamespacesAsAttributes;
protected abstract String getOutputFormat();
protected abstract String getDoctypePublic();
protected abstract String getDoctypeSystem();
/**
* @see org.apache.sling.rewriter.SerializerFactory#createSerializer()
*/
public Serializer createSerializer() {
TransformerHandler tHandler = null;
try {
tHandler = this.tfactory.newTransformerHandler();
} catch (TransformerConfigurationException e) {
logger.error("Unable to create new transformer handler.", e);
}
final ContentHandler ch;
if ( this.needsNamespacesAsAttributes ) {
final NamespaceAsAttributes nsPipeline = new NamespaceAsAttributes(tHandler, this.logger);
ch = nsPipeline;
} else {
ch = tHandler;
}
return new TraxSerializer(tHandler, ch, getOutputFormat(), getDoctypePublic(), getDoctypeSystem());
}
protected void activate(final ComponentContext ctx) {
this.tfactory = (SAXTransformerFactory) TransformerFactory.newInstance();
tfactory.setErrorListener(new TraxErrorHandler(this.logger));
// Check if we need namespace as attributes.
try {
this.needsNamespacesAsAttributes = this.needsNamespacesAsAttributes();
} catch (Exception e) {
this.logger.warn("Cannot know if transformer needs namespaces attributes - assuming NO.", e);
this.needsNamespacesAsAttributes = false;
}
}
protected void deactivat(final ComponentContext ctx) {
this.tfactory = null;
}
/**
* 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 {
// Serialize a minimal document to check how namespaces are handled.
final StringWriter writer = new StringWriter();
final String uri = "namespaceuri";
final String prefix = "nsp";
final String check = "xmlns:" + prefix + "='" + uri + "'";
final TransformerHandler handler = this.tfactory.newTransformerHandler();
handler.setResult(new StreamResult(writer));
// Output a single element
handler.startDocument();
handler.startPrefixMapping(prefix, uri);
handler.startElement(uri, "element", "element", new AttributesImpl());
handler.endElement(uri, "element", "element");
handler.endPrefixMapping(prefix);
handler.endDocument();
final String text = writer.toString();
// Check if the namespace is there (replace " by ' to be sure of what we search in)
return (text.replace('"', '\'').indexOf(check) == -1);
}
//--------------------------------------------------------------------------------------------
/**
* 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 implements ContentHandler, LexicalHandler {
/** The URI for xml namespaces */
private static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
/**
* The prefixes of startPrefixMapping() declarations for the coming element.
*/
private List<String> prefixList = new ArrayList<String>();
/**
* The URIs of startPrefixMapping() declarations for the coming element.
*/
private List<String> uriList = new ArrayList<String>();
/**
* Maps of URI<->prefix mappings. Used to work around a bug in the Xalan
* serializer.
*/
private Map<String, String> uriToPrefixMap = new HashMap<String, String>();
private Map<String, String> prefixToUriMap = new HashMap<String, String>();
/**
* True if there has been some startPrefixMapping() for the coming element.
*/
private boolean hasMappings = false;
protected final ContentHandler contentHandler;
protected final LexicalHandler lexicalHandler;
protected final Logger logger;
public NamespaceAsAttributes(final ContentHandler handler, final Logger logger) {
this.contentHandler = handler;
this.lexicalHandler = (LexicalHandler)handler;
this.logger = logger;
}
/**
* @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
*/
public void setDocumentLocator(Locator locator) {
contentHandler.setDocumentLocator(locator);
}
/**
* Receive notification of character data.
*
* @param c The characters from the XML document.
* @param start The start position in the array.
* @param len The number of characters to read from the array.
*/
public void characters(char c[], int start, int len)
throws SAXException {
contentHandler.characters(c, start, len);
}
/**
* Receive notification of ignorable whitespace in element content.
*
* @param c The characters from the XML document.
* @param start The start position in the array.
* @param len The number of characters to read from the array.
*/
public void ignorableWhitespace(char c[], int start, int len)
throws SAXException {
contentHandler.ignorableWhitespace(c, start, len);
}
/**
* Receive notification of a processing instruction.
*
* @param target The processing instruction target.
* @param data The processing instruction data, or null if none was
* supplied.
*/
public void processingInstruction(String target, String data)
throws SAXException {
contentHandler.processingInstruction(target, data);
}
/**
* Receive notification of a skipped entity.
*
* @param name The name of the skipped entity. If it is a parameter
* entity, the name will begin with '%'.
*/
public void skippedEntity(String name)
throws SAXException {
contentHandler.skippedEntity(name);
}
/**
* Report the start of DTD declarations, if any.
*
* @param name The document type name.
* @param publicId The declared public identifier for the external DTD
* subset, or null if none was declared.
* @param systemId The declared system identifier for the external DTD
* subset, or null if none was declared.
*/
public void startDTD(String name, String publicId, String systemId)
throws SAXException {
lexicalHandler.startDTD(name, publicId, systemId);
}
/**
* Report the end of DTD declarations.
*/
public void endDTD()
throws SAXException {
lexicalHandler.endDTD();
}
/**
* Report the beginning of an entity.
*
* @param name The name of the entity. If it is a parameter entity, the
* name will begin with '%'.
*/
public void startEntity(String name)
throws SAXException {
lexicalHandler.startEntity(name);
}
/**
* Report the end of an entity.
*
* @param name The name of the entity that is ending.
*/
public void endEntity(String name)
throws SAXException {
lexicalHandler.endEntity(name);
}
/**
* Report the start of a CDATA section.
*/
public void startCDATA()
throws SAXException {
lexicalHandler.startCDATA();
}
/**
* Report the end of a CDATA section.
*/
public void endCDATA()
throws SAXException {
lexicalHandler.endCDATA();
}
/**
* Report an XML comment anywhere in the document.
*
* @param ch An array holding the characters in the comment.
* @param start The starting position in the array.
* @param len The number of characters to use from the array.
*/
public void comment(char ch[], int start, int len)
throws SAXException {
lexicalHandler.comment(ch, start, len);
}
public void startDocument() throws SAXException {
// Cleanup
this.uriToPrefixMap.clear();
this.prefixToUriMap.clear();
clearMappings();
this.contentHandler.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);
}
this.contentHandler.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 = this.uriList.get(mapping);
String prefix = 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))) {
logger.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(XML_NAMESPACE_URI, "xmlns", "xmlns", "CDATA", uri);
} else {
newAttrs.addAttribute(XML_NAMESPACE_URI, prefix, qName, "CDATA", uri);
}
}
} // end for mapping
// Cleanup for the next element
clearMappings();
// Start element with new attributes, if any
this.contentHandler.startElement(eltUri, eltLocalName, eltQName, newAttrs == null ? attrs : newAttrs);
} else {
// Normal job
this.contentHandler.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;
}
this.contentHandler.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);
}
}
this.contentHandler.endPrefixMapping(prefix);
}
/**
*
*/
public void endDocument() throws SAXException {
// Cleanup
this.uriToPrefixMap.clear();
this.prefixToUriMap.clear();
clearMappings();
this.contentHandler.endDocument();
}
private void clearMappings() {
this.hasMappings = false;
this.prefixList.clear();
this.uriList.clear();
}
}
}