Mega Code Archive

 
Categories / Java / XML
 

Utility class for xmlsax handling

/*  * 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      * <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 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 <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 = (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<level;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");     } }