/*
* 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.
*/
import java.io.*;
import java.util.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl;
/**
* Utility class for xml/sax handling.
* It provides support for "older" sax implementations (like the default one shipped with JDK 1.4.2)
* which have bugs in the namespace handling.
*/
public class IOUtils {
/** The transformer factory. */
private static final SAXTransformerFactory FACTORY = (SAXTransformerFactory) TransformerFactory.newInstance();
/** The URI for xml namespaces */
private static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
/**
* Parse a file and send the sax events to the content handler.
* @param file
* @param handler
* @throws IOException
* @throws TransformerException
*/
public static final void parse(File file, ContentHandler handler)
throws IOException, TransformerException {
final Transformer transformer = FACTORY.newTransformer();
transformer.transform(new StreamSource(new FileReader(file)),
new SAXResult(handler));
}
public static ContentHandler getSerializer(File file)
throws IOException, TransformerException {
final FileWriter writer = new FileWriter(file);
final TransformerHandler transformerHandler = FACTORY.newTransformerHandler();
final Transformer transformer = transformerHandler.getTransformer();
final Properties format = new Properties();
format.put(OutputKeys.METHOD, "xml");
format.put(OutputKeys.OMIT_XML_DECLARATION, "no");
format.put(OutputKeys.ENCODING, "UTF-8");
format.put(OutputKeys.INDENT, "yes");
transformer.setOutputProperties(format);
transformerHandler.setResult(new StreamResult(writer));
try {
if ( needsNamespacesAsAttributes(format) ) {
return new NamespaceAsAttributes(transformerHandler);
}
} catch (SAXException se) {
throw new TransformerException("Unable to detect of namespace support for sax works properly.", se);
}
return transformerHandler;
}
/**
* Checks if the used Trax implementation correctly handles namespaces set using
* startPrefixMapping()
, but wants them also as 'xmlns:' attributes.
*
* The check consists in sending SAX events representing a minimal namespaced document
* with namespaces defined only with calls to startPrefixMapping
(no
* xmlns:xxx attributes) and check if they are present in the resulting text.
*/
protected static boolean needsNamespacesAsAttributes(Properties format)
throws TransformerException, SAXException {
// 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 = FACTORY.newTransformerHandler();
handler.getTransformer().setOutputProperties(format);
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)
boolean needsIt = (text.replace('"', '\'').indexOf(check) == -1);
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 implements ContentHandler {
/** The wrapped content handler. */
private final ContentHandler contentHandler;
/**
* 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 NamespaceAsAttributes(ContentHandler ch) {
this.contentHandler = ch;
}
public void startDocument() throws SAXException {
// Cleanup
this.uriToPrefixMap.clear();
this.prefixToUriMap.clear();
clearMappings();
this.contentHandler.startDocument();
}
/**
* Track mappings to be able to add xmlns:
attributes
* in startElement()
.
*/
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 xmlns:
attributes
* and add those needed before calling superclass. This is a workaround for a Xalan bug
* (at least in version 2.0.1) : org.apache.xalan.serialize.SerializerToXML
* ignores start/endPrefixMapping()
.
*/
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.length() == 0 ? "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))) {
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.length() == 0) {
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);
}
/**
* @see org.xml.sax.ContentHandler#endDocument()
*/
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();
}
/**
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
*/
public void characters(char[] ch, int start, int length) throws SAXException {
contentHandler.characters(ch, start, length);
}
/**
* @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
*/
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
contentHandler.ignorableWhitespace(ch, start, length);
}
/**
* @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
*/
public void processingInstruction(String target, String data) throws SAXException {
contentHandler.processingInstruction(target, data);
}
/**
* @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
*/
public void setDocumentLocator(Locator locator) {
contentHandler.setDocumentLocator(locator);
}
/**
* @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
*/
public void skippedEntity(String name) throws SAXException {
contentHandler.skippedEntity(name);
}
}
/**
* Helper method to add an attribute.
* This implementation adds a new attribute with the given name
* and value. Before adding the value is checked for non-null.
* @param ai The attributes impl receiving the additional attribute.
* @param name The name of the attribute.
* @param value The value of the attribute.
*/
protected static void addAttribute(AttributesImpl ai, String name, Object value) {
if ( value != null ) {
ai.addAttribute("", name, name, "CDATA", value.toString());
}
}
/**
* Helper method writing out a string.
* @param ch The content handler.
* @param text
* @throws SAXException
*/
protected static void text(ContentHandler ch, String text)
throws SAXException {
if ( text != null ) {
final char[] c = text.toCharArray();
ch.characters(c, 0, c.length);
}
}
/**
* Helper method to indent the xml elements.
* Each level is indented with four spaces.
* @param ch The content handler.
* @param level The level of indention.
*/
protected static void indent(ContentHandler ch, int level)
throws SAXException {
for(int i=0;i IOUtils.text(ch, " ");
}
}
/**
* Helper method to create a new line.
* @param ch The content handler.
* @throws SAXException
*/
protected static void newline(ContentHandler ch)
throws SAXException {
IOUtils.text(ch, "\n");
}
}