All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.xml.XMLUtil Maven / Gradle / Ivy

There is a newer version: RELEASE230
Show newest version
/*
 * 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.openide.xml;

import java.io.ByteArrayInputStream;
import java.io.CharConversionException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.Validator;
import org.openide.util.Exceptions;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

/**
 * Utility class collecting library methods related to XML processing.
 */
public final class XMLUtil extends Object {

    /*
        public static String toCDATA(String val) throws IOException {

        }
    */
    private static final char[] DEC2HEX = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };

    /** Forbids creating new XMLUtil */
    private XMLUtil() {
    }

    // ~~~~~~~~~~~~~~~~~~~~~ SAX related ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /** Create a simple parser.
      * @return createXMLReader(false, false)
      */
    public static XMLReader createXMLReader() throws SAXException {
        return createXMLReader(false, false);
    }

    /** Create a simple parser, possibly validating.
     * @param validate if true, a validating parser is returned
     * @return createXMLReader(validate, false)
     */
    public static XMLReader createXMLReader(boolean validate)
    throws SAXException {
        return createXMLReader(validate, false);
    }

    private static SAXParserFactory[][] saxes  = new SAXParserFactory[2][2];
    /** Creates a SAX parser.
     *
     * 

See {@link #parse} for hints on setting an entity resolver. * * @param validate if true, a validating parser is returned * @param namespaceAware if true, a namespace aware parser is returned * * @throws FactoryConfigurationError Application developers should never need to directly catch errors of this type. * @throws SAXException if a parser fulfilling given parameters can not be created * * @return XMLReader configured according to passed parameters */ public static synchronized XMLReader createXMLReader(boolean validate, boolean namespaceAware) throws SAXException { SAXParserFactory factory = saxes[validate ? 0 : 1][namespaceAware ? 0 : 1]; if (factory == null) { try { factory = SAXParserFactory.newInstance(); } catch (FactoryConfigurationError err) { Exceptions.attachMessage( err, "Info about thread context classloader: " + // NOI18N Thread.currentThread().getContextClassLoader() ); throw err; } factory.setValidating(validate); factory.setNamespaceAware(namespaceAware); saxes[validate ? 0 : 1][namespaceAware ? 0 : 1] = factory; } try { return factory.newSAXParser().getXMLReader(); } catch (ParserConfigurationException ex) { throw new SAXException("Cannot create parser satisfying configuration parameters", ex); //NOI18N } } // ~~~~~~~~~~~~~~~~~~~~~ DOM related ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * Creates an empty DOM document. E.g.: *

     * Document doc = createDocument("book", null, null, null);
     * 

* creates new DOM of a well-formed document with root element named book. * * @param rootQName qualified name of root element. e.g. myroot or ns:myroot * @param namespaceURI URI of root element namespace or null * @param doctypePublicID public ID of DOCTYPE or null * @param doctypeSystemID system ID of DOCTYPE or null if no DOCTYPE * required and doctypePublicID is also null * * @throws DOMException if new DOM with passed parameters can not be created * @throws FactoryConfigurationError Application developers should never need to directly catch errors of this type. * * @return new DOM Document */ public static Document createDocument( String rootQName, String namespaceURI, String doctypePublicID, String doctypeSystemID ) throws DOMException { DOMImplementation impl = getDOMImplementation(); if ((doctypePublicID != null) && (doctypeSystemID == null)) { throw new IllegalArgumentException("System ID cannot be null if public ID specified. "); //NOI18N } DocumentType dtd = null; if (doctypeSystemID != null) { dtd = impl.createDocumentType(rootQName, doctypePublicID, doctypeSystemID); } return impl.createDocument(namespaceURI, rootQName, dtd); } /** * Obtains DOMImpementaton interface providing a number of methods for performing * operations that are independent of any particular DOM instance. * * @throw DOMException NOT_SUPPORTED_ERR if cannot get DOMImplementation * @throw FactoryConfigurationError Application developers should never need to directly catch errors of this type. * * @return DOMImplementation implementation */ private static DOMImplementation getDOMImplementation() throws DOMException { //can be made public DocumentBuilderFactory factory = getFactory(false, false); try { return factory.newDocumentBuilder().getDOMImplementation(); } catch (ParserConfigurationException ex) { throw new DOMException( DOMException.NOT_SUPPORTED_ERR, "Cannot create parser satisfying configuration parameters" ); //NOI18N } catch (RuntimeException e) { // E.g. #36578, IllegalArgumentException. Try to recover gracefully. throw (DOMException) new DOMException(DOMException.NOT_SUPPORTED_ERR, e.toString()).initCause(e); } } private static DocumentBuilderFactory[][] doms = new DocumentBuilderFactory[2][2]; private static synchronized DocumentBuilderFactory getFactory(boolean validate, boolean namespaceAware) { DocumentBuilderFactory factory = doms[validate ? 0 : 1][namespaceAware ? 0 : 1]; if (factory == null) { factory = DocumentBuilderFactory.newInstance(); factory.setValidating(validate); factory.setNamespaceAware(namespaceAware); doms[validate ? 0 : 1][namespaceAware ? 0 : 1] = factory; } return factory; } /** * Parses an XML document into a DOM tree. * *

* *

Remember that when parsing XML files you often want to set an explicit * entity resolver. For example, consider a file such as this:

* *
     * <?xml version="1.0" encoding="UTF-8"?>
     * <!DOCTYPE root PUBLIC "-//NetBeans//DTD Foo 1.0//EN" "http://www.netbeans.org/dtds/foo-1_0.dtd">
     * <root/>
     * 
* *

If you parse this with a null entity resolver, or you use the * default resolver ({@link EntityCatalog#getDefault}) but do not do * anything special with this DTD, you will probably find the parse * blocking to make a network connection even when you are not * validating. That is because DTDs can be used to define * entities and other XML oddities, and are not a pure constraint * language like Schema or RELAX-NG.

* *

There are three basic ways to avoid the network connection.

* *
    * *
  1. Register the DTD. This is generally the best thing to do. See * {@link EntityCatalog}'s documentation for details, but for example * in your layer use:

    * *
         * <filesystem>
         *   <folder name="xml">
         *     <folder name="entities">
         *       <folder name="NetBeans">
         *         <file name="DTD_Foo_1_0"
         *               url="resources/foo-1_0.dtd">
         *           <attr name="hint.originalPublicID"
         *                 stringvalue="-//NetBeans//DTD Foo 1.0//EN"/>
         *         </file>
         *       </folder>
         *     </folder>
         *   </folder>
         * </filesystem>
         * 
    * *

    Now the default system entity catalog will resolve the public ID * to the local copy in your module, not the network copy. * Additionally, anyone who mounts the "NetBeans Catalog" in the XML * Entity Catalogs node in the Runtime tab will be able to use your * local copy of the DTD automatically, for validation, code * completion, etc. (The network URL should really exist, though, for * the benefit of other tools!)

  2. * *
  3. You can also set an explicit entity resolver which maps that * particular public ID to some local copy of the DTD, if you do not * want to register it globally in the system for some reason. If * handed other public IDs, just return null to indicate that the * system ID should be loaded.

  4. * *
  5. In some cases where XML parsing is very * performance-sensitive, and you know that you do not need validation * and furthermore that the DTD defines no infoset (there are no * entity or character definitions, etc.), you can speed up the parse. * Turn off validation, but also supply a custom entity resolver that * does not even bother to load the DTD at all:

    * *
         * public InputSource resolveEntity(String pubid, String sysid)
         *     throws SAXException, IOException {
         *   if (pubid.equals("-//NetBeans//DTD Foo 1.0//EN")) {
         *     return new InputSource(new ByteArrayInputStream(new byte[0]));
         *   } else {
         *     return EntityCatalog.getDefault().resolveEntity(pubid, sysid);
         *   }
         * }
         * 
  6. * *
* *
* * @param input a parser input (for URL users use: new InputSource(url.toString()) * @param validate if true validating parser is used * @param namespaceAware if true DOM is created by namespace aware parser * @param errorHandler a error handler to notify about exception (such as {@link #defaultErrorHandler}) or null * @param entityResolver SAX entity resolver (such as {@link EntityCatalog#getDefault}) or null * * @throws IOException if an I/O problem during parsing occurs * @throws SAXException is thrown if a parser error occurs * @throws FactoryConfigurationError Application developers should never need to directly catch errors of this type. * * @return document representing given input */ public static Document parse( InputSource input, boolean validate, boolean namespaceAware, ErrorHandler errorHandler, EntityResolver entityResolver ) throws IOException, SAXException { DocumentBuilder builder = null; DocumentBuilderFactory factory = getFactory(validate, namespaceAware); try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new SAXException("Cannot create parser satisfying configuration parameters", ex); //NOI18N } if (errorHandler != null) { builder.setErrorHandler(errorHandler); } if (entityResolver != null) { builder.setEntityResolver(entityResolver); } return builder.parse(input); } /** * Identity transformation in XSLT with indentation added. * Just using the identity transform and calling * t.setOutputProperty(OutputKeys.INDENT, "yes"); * t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); * does not work currently. * You really have to use this bogus stylesheet. * @see "JDK bug #5064280" */ private static final String IDENTITY_XSLT_WITH_INDENT = "" + // NOI18N "" + // NOI18N "" + // NOI18N "" + // NOI18N "" + // NOI18N "" + // NOI18N "" + // NOI18N ""; // NOI18N /** Workaround for JAXP bug 7150637 / XALANJ-1497. */ private static final String ORACLE_IS_STANDALONE = "http://www.oracle.com/xml/is-standalone"; /** * Writes a DOM document to a stream. * The precise output format is not guaranteed but this method will attempt to indent it sensibly. * *

Important: There might be some problems with * <![CDATA[ ]]> sections in the DOM tree you pass into this method. Specifically, * some CDATA sections my not be written as CDATA section or may be merged with * other CDATA section at the same level. Also if plain text nodes are mixed with * CDATA sections at the same level all text is likely to end up in one big CDATA section. *
* For nodes that only have one CDATA section this method should work fine. *

* * @param doc DOM document to be written * @param out data sink * @param enc XML-defined encoding name (e.g. "UTF-8") * @throws IOException if JAXP fails or the stream cannot be written to */ public static void write(Document doc, OutputStream out, String enc) throws IOException { if (enc == null) { throw new NullPointerException("You must set an encoding; use \"UTF-8\" unless you have a good reason not to!"); // NOI18N } Document doc2 = normalize(doc); ClassLoader orig = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(AccessController.doPrivileged(new PrivilegedAction() { // #195921 @Override public ClassLoader run() { return new ClassLoader(ClassLoader.getSystemClassLoader().getParent()) { @Override public InputStream getResourceAsStream(String name) { if (name.startsWith("META-INF/services/")) { return new ByteArrayInputStream(new byte[0]); // JAXP #6723276 } return super.getResourceAsStream(name); } }; } })); try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer t = tf.newTransformer( new StreamSource(new StringReader(IDENTITY_XSLT_WITH_INDENT))); DocumentType dt = doc2.getDoctype(); if (dt != null) { String pub = dt.getPublicId(); if (pub != null) { t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, pub); } String sys = dt.getSystemId(); if (sys != null) { t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, sys); } } t.setOutputProperty(OutputKeys.ENCODING, enc); try { t.setOutputProperty(ORACLE_IS_STANDALONE, "yes"); } catch (IllegalArgumentException x) { // fine, introduced in JDK 7u4 } // See #123816 Set cdataQNames = new HashSet(); collectCDATASections(doc2, cdataQNames); if (cdataQNames.size() > 0) { StringBuilder cdataSections = new StringBuilder(); for(String s : cdataQNames) { cdataSections.append(s).append(' '); //NOI18N } t.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, cdataSections.toString()); } Source source = new DOMSource(doc2); Result result = new StreamResult(out); t.transform(source, result); } catch (Exception e) { throw new IOException(e); } finally { Thread.currentThread().setContextClassLoader(orig); } } private static void collectCDATASections(Node node, Set cdataQNames) { if (node instanceof CDATASection) { Node parent = node.getParentNode(); if (parent != null) { String uri = parent.getNamespaceURI(); if (uri != null) { cdataQNames.add("{" + uri + "}" + parent.getNodeName()); //NOI18N } else { cdataQNames.add(parent.getNodeName()); } } } NodeList children = node.getChildNodes(); for(int i = 0; i < children.getLength(); i++) { collectCDATASections(children.item(i), cdataQNames); } } /** * Check whether a DOM tree is valid according to a schema. * Example of usage: *
     * Element fragment = ...;
     * SchemaFactory f = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
     * Schema s = f.newSchema(This.class.getResource("something.xsd"));
     * try {
     *     XMLUtil.validate(fragment, s);
     *     // valid
     * } catch (SAXException x) {
     *     // invalid
     * }
     * 
* @param data a DOM tree * @param schema a parsed schema * @throws SAXException if validation failed * @since org.openide.util 7.17 */ public static void validate(Element data, Schema schema) throws SAXException { Validator v = schema.newValidator(); final SAXException[] error = {null}; v.setErrorHandler(new ErrorHandler() { public @Override void warning(SAXParseException x) throws SAXException {} public @Override void error(SAXParseException x) throws SAXException { // Just rethrowing it is bad because it will also print it to stderr. error[0] = x; } public @Override void fatalError(SAXParseException x) throws SAXException { error[0] = x; } }); try { v.validate(new DOMSource(fixupAttrs(data))); } catch (IOException x) { assert false : x; } if (error[0] != null) { throw error[0]; } } private static Element fixupAttrs(Element root) { // #140905 // #6529766/#6531160: some versions of JAXP reject attributes set using setAttribute // (rather than setAttributeNS) even though the schema calls for no-NS attrs! // JDK 5 is fine; JDK 6 broken; JDK 6u2+ fixed // #146081: xml:base attributes mess up validation too. Element copy = (Element) root.cloneNode(true); fixupAttrsSingle(copy); NodeList nl = copy.getElementsByTagName("*"); // NOI18N for (int i = 0; i < nl.getLength(); i++) { fixupAttrsSingle((Element) nl.item(i)); } return copy; } private static void fixupAttrsSingle(Element e) throws DOMException { removeXmlBase(e); Map replace = new HashMap(); NamedNodeMap attrs = e.getAttributes(); for (int j = 0; j < attrs.getLength(); j++) { Attr attr = (Attr) attrs.item(j); if (attr.getNamespaceURI() == null && !attr.getName().equals("xmlns")) { // NOI18N replace.put(attr.getName(), attr.getValue()); } } for (Map.Entry entry : replace.entrySet()) { e.removeAttribute(entry.getKey()); e.setAttributeNS(null, entry.getKey(), entry.getValue()); } } private static void removeXmlBase(Element e) { e.removeAttributeNS("http://www.w3.org/XML/1998/namespace", "base"); // NOI18N e.removeAttribute("xml:base"); // NOI18N } /** * Escape passed string as XML attibute value * (<, &, ' and " * will be escaped. * Note: An XML processor returns normalized value that can be different. * * @param val a string to be escaped * * @return escaped value * @throws CharConversionException if val contains an improper XML character * * @since 1.40 */ public static String toAttributeValue(String val) throws CharConversionException { if (val == null) { throw new CharConversionException("null"); // NOI18N } if (checkAttributeCharacters(val)) { return val; } StringBuilder buf = new StringBuilder(); for (int i = 0; i < val.length(); i++) { char ch = val.charAt(i); if ('<' == ch) { buf.append("<"); continue; } else if ('&' == ch) { buf.append("&"); continue; } else if ('\'' == ch) { buf.append("'"); continue; } else if ('"' == ch) { buf.append("""); continue; } buf.append(ch); } return buf.toString(); } /** * Escape passed string as XML element content (<, * & and > in ]]> sequences). * * @param val a string to be escaped * * @return escaped value * @throws CharConversionException if val contains an improper XML character * * @since 1.40 */ public static String toElementContent(String val) throws CharConversionException { if (val == null) { throw new CharConversionException("null"); // NOI18N } if (checkContentCharacters(val)) { return val; } StringBuilder buf = new StringBuilder(); for (int i = 0; i < val.length(); i++) { char ch = val.charAt(i); if ('<' == ch) { buf.append("<"); continue; } else if ('&' == ch) { buf.append("&"); continue; } else if (('>' == ch) && (i > 1) && (val.charAt(i - 2) == ']') && (val.charAt(i - 1) == ']')) { buf.append(">"); continue; } buf.append(ch); } return buf.toString(); } /** * Can be used to encode values that contain invalid XML characters. * At SAX parser end must be used pair method to get original value. * * @param val data to be converted * @param start offset * @param len count * * @since 1.29 */ public static String toHex(byte[] val, int start, int len) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < len; i++) { byte b = val[start + i]; buf.append(DEC2HEX[(b & 0xf0) >> 4]); buf.append(DEC2HEX[b & 0x0f]); } return buf.toString(); } /** * Decodes data encoded using {@link #toHex(byte[],int,int) toHex}. * * @param hex data to be converted * @param start offset * @param len count * * @throws IOException if input does not represent hex encoded value * * @since 1.29 */ public static byte[] fromHex(char[] hex, int start, int len) throws IOException { if (hex == null) { throw new IOException("null"); } int i = hex.length; if ((i % 2) != 0) { throw new IOException("odd length"); } byte[] magic = new byte[i / 2]; for (; i > 0; i -= 2) { String g = new String(hex, i - 2, 2); try { magic[(i / 2) - 1] = (byte) Integer.parseInt(g, 16); } catch (NumberFormatException ex) { throw new IOException(ex.getLocalizedMessage()); } } return magic; } /** * Check if all passed characters match XML expression [2]. * @return true if no escaping necessary * @throws CharConversionException if contains invalid chars */ private static boolean checkAttributeCharacters(String chars) throws CharConversionException { boolean escape = false; for (int i = 0; i < chars.length(); i++) { char ch = chars.charAt(i); if (((int) ch) <= 93) { // we are UNICODE ']' switch (ch) { case 0x9: case 0xA: case 0xD: continue; case '\'': case '"': case '<': case '&': escape = true; continue; default: if (((int) ch) < 0x20) { throw new CharConversionException("Invalid XML character &#" + ((int) ch) + ";."); } } } } return escape == false; } /** * Check if all passed characters match XML expression [2]. * @return true if no escaping necessary * @throws CharConversionException if contains invalid chars */ private static boolean checkContentCharacters(String chars) throws CharConversionException { boolean escape = false; for (int i = 0; i < chars.length(); i++) { char ch = chars.charAt(i); if (((int) ch) <= 93) { // we are UNICODE ']' switch (ch) { case 0x9: case 0xA: case 0xD: continue; case '>': // only ]]> is dangerous if (escape) { continue; } escape = (i > 0) && (chars.charAt(i - 1) == ']'); continue; case '<': case '&': escape = true; continue; default: if (((int) ch) < 0x20) { throw new CharConversionException("Invalid XML character &#" + ((int) ch) + ";."); } } } } return escape == false; } /** * Try to normalize a document by removing nonsignificant whitespace. * @see "#62006" */ private static Document normalize(Document orig) throws IOException { DocumentBuilder builder = null; DocumentBuilderFactory factory = getFactory(false, false); try { builder = factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new IOException("Cannot create parser satisfying configuration parameters: " + e, e); //NOI18N } DocumentType doctype = null; NodeList nl = orig.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { if (nl.item(i) instanceof DocumentType) { // We cannot import DocumentType's, so we need to manually copy it. doctype = (DocumentType) nl.item(i); } } Document doc; if (doctype != null) { doc = builder.getDOMImplementation().createDocument( orig.getDocumentElement().getNamespaceURI(), orig.getDocumentElement().getTagName(), builder.getDOMImplementation().createDocumentType( orig.getDoctype().getName(), orig.getDoctype().getPublicId(), orig.getDoctype().getSystemId())); // XXX what about entity decls inside the DOCTYPE? doc.removeChild(doc.getDocumentElement()); } else { doc = builder.newDocument(); } for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (!(node instanceof DocumentType)) { try { doc.appendChild(doc.importNode(node, true)); } catch (DOMException x) { // Thrown in NB-Core-Build #2896 & 2898 inside GeneratedFilesHelper.applyBuildExtensions throw new IOException("Could not import or append " + node + " of " + node.getClass(), x); } } } doc.normalize(); nl = doc.getElementsByTagName("*"); // NOI18N for (int i = 0; i < nl.getLength(); i++) { Element e = (Element) nl.item(i); removeXmlBase(e); NodeList nl2 = e.getChildNodes(); for (int j = 0; j < nl2.getLength(); j++) { Node n = nl2.item(j); if (n instanceof Text && ((Text) n).getNodeValue().trim().length() == 0) { e.removeChild(n); j--; // since list is dynamic } } } return doc; } /** * Append a child element to the parent at the specified location. * * Starting with a valid document, append an element according to the schema * sequence represented by the order. All existing child elements must be * include as well as the new element. The existing child element following * the new child is important, as the element will be 'inserted before', not * 'inserted after'. * * @param parent parent to which the child will be appended * @param el element to be added * @param order order of the elements which must be followed * @throws IllegalArgumentException if the order cannot be followed, either * a missing existing or new child element is not specified in order * * @since 8.4 */ public static void appendChildElement(Element parent, Element el, String[] order) throws IllegalArgumentException { List l = Arrays.asList(order); int index = l.indexOf(el.getLocalName()); // ensure the new new element is contained in the 'order' if (index == -1) { throw new IllegalArgumentException("new child element '"+ el.getLocalName() + "' not specified in order " + l); // NOI18N } List elements = findSubElements(parent); Element insertBefore = null; for (Element e : elements) { int index2 = l.indexOf(e.getLocalName()); // ensure that all existing elements are in 'order' if (index2 == -1) { throw new IllegalArgumentException("Existing child element '" + e.getLocalName() + "' not specified in order " + l); // NOI18N } if (index2 > index) { insertBefore = e; break; } } parent.insertBefore(el, insertBefore); } /** * Find all direct child elements of an element. * Children which are all-whitespace text nodes or comments are ignored; others cause * an exception to be thrown. * @param parent a parent element in a DOM tree * @return a list of direct child elements (may be empty) * @throws IllegalArgumentException if there are non-element children besides whitespace * * @since 8.4 */ public static List findSubElements(Element parent) throws IllegalArgumentException { NodeList l = parent.getChildNodes(); List elements = new ArrayList(l.getLength()); for (int i = 0; i < l.getLength(); i++) { Node n = l.item(i); if (n.getNodeType() == Node.ELEMENT_NODE) { elements.add((Element) n); } else if (n.getNodeType() == Node.TEXT_NODE) { String text = ((Text) n).getNodeValue(); if (text.trim().length() > 0) { throw new IllegalArgumentException("non-ws text encountered in " + parent + ": " + text); // NOI18N } } else if (n.getNodeType() == Node.COMMENT_NODE) { // OK, ignore } else { throw new IllegalArgumentException("unexpected non-element child of " + parent + ": " + n); // NOI18N } } return elements; } /** * Search for an XML element in the direct children of parent only. * * This compares localName (nodeName if localName is null) to name, * and checks the tags namespace with the provided namespace. * A null namespace will match any namespace. * *
    This is differs from the DOM version by: *
  • not searching recursively
  • *
  • returns a single result
  • *
* * @param parent a parent element * @param name the intended local name * @param namespace the intended namespace (or null) * @return the one child element with that name, or null if none * @throws IllegalArgumentException if there is multiple elements of the same name * * @since 8.4 */ public static Element findElement(Element parent, String name, String namespace) throws IllegalArgumentException { Element result = null; NodeList l = parent.getChildNodes(); int nodeCount = l.getLength(); for (int i = 0; i < nodeCount; i++) { if (l.item(i).getNodeType() == Node.ELEMENT_NODE) { Node node = l.item(i); String localName = node.getLocalName(); localName = localName == null ? node.getNodeName() : localName; if (name.equals(localName) && (namespace == null || namespace.equals(node.getNamespaceURI()))) { if (result == null) { result = (Element)node; } else { throw new IllegalArgumentException("more than one element with same name found"); } } } } return result; } /** * Extract nested text from a node. * Currently does not handle coalescing text nodes, CDATA sections, etc. * @param parent a parent element * @return the nested text, or null if none was found * * @since 8.4 */ public static String findText(Node parent) { NodeList l = parent.getChildNodes(); for (int i = 0; i < l.getLength(); i++) { if (l.item(i).getNodeType() == Node.TEXT_NODE) { Text text = (Text) l.item(i); return text.getNodeValue(); } } return null; } /** * Convert an XML fragment from one namespace to another. * * @param from element to translate * @param namespace namespace to be translated to * @return * * @since 8.4 */ public static Element translateXML(Element from, String namespace) { Element to = from.getOwnerDocument().createElementNS(namespace, from.getLocalName()); NodeList nl = from.getChildNodes(); int length = nl.getLength(); for (int i = 0; i < length; i++) { Node node = nl.item(i); Node newNode; if (node.getNodeType() == Node.ELEMENT_NODE) { newNode = translateXML((Element) node, namespace); } else { newNode = node.cloneNode(true); } to.appendChild(newNode); } NamedNodeMap m = from.getAttributes(); for (int i = 0; i < m.getLength(); i++) { Node attr = m.item(i); to.setAttribute(attr.getNodeName(), attr.getNodeValue()); } return to; } /** * Copy elements from one document to another attaching at the specified element * and translating the namespace. * * @param from copy the children of this element (exclusive) * @param to where to attach the copied elements * @param newNamespace destination namespace * * @since 8.4 */ public static void copyDocument(Element from, Element to, String newNamespace) { Document doc = to.getOwnerDocument(); NodeList nl = from.getChildNodes(); int length = nl.getLength(); for (int i = 0; i < length; i++) { Node node = nl.item(i); Node newNode = null; if (Node.ELEMENT_NODE == node.getNodeType()) { Element oldElement = (Element) node; newNode = doc.createElementNS(newNamespace, oldElement.getTagName()); NamedNodeMap m = oldElement.getAttributes(); Element newElement = (Element) newNode; for (int index = 0; index < m.getLength(); index++) { Node attr = m.item(index); newElement.setAttribute(attr.getNodeName(), attr.getNodeValue()); } copyDocument(oldElement, newElement, newNamespace); } else { newNode = node.cloneNode(true); newNode = to.getOwnerDocument().importNode(newNode, true); } if (newNode != null) { to.appendChild(newNode); } } } /** * Create an XML error handler that rethrows errors and fatal errors and logs warnings. * @return a standard error handler * * @since 8.4 */ public static ErrorHandler defaultErrorHandler() { return new ErrHandler(); } private static final class ErrHandler implements ErrorHandler { ErrHandler() {} private void annotate(SAXParseException exception) throws SAXException { Exceptions.attachMessage(exception, "Occurred at: " + exception.getSystemId() + ":" + exception.getLineNumber()); // NOI18N } public @Override void fatalError(SAXParseException exception) throws SAXException { annotate(exception); throw exception; } public @Override void error(SAXParseException exception) throws SAXException { annotate(exception); throw exception; } public @Override void warning(SAXParseException exception) throws SAXException { annotate(exception); Logger.getLogger(XMLUtil.class.getName()).log(Level.INFO, null, exception); } } }