001/*
002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003 *
004 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
005 *
006 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
007 * Other names may be trademarks of their respective owners.
008 *
009 * The contents of this file are subject to the terms of either the GNU
010 * General Public License Version 2 only ("GPL") or the Common
011 * Development and Distribution License("CDDL") (collectively, the
012 * "License"). You may not use this file except in compliance with the
013 * License. You can obtain a copy of the License at
014 * http://www.netbeans.org/cddl-gplv2.html
015 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
016 * specific language governing permissions and limitations under the
017 * License.  When distributing the software, include this License Header
018 * Notice in each file and include the License file at
019 * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
020 * particular file as subject to the "Classpath" exception as provided
021 * by Oracle in the GPL Version 2 section of the License file that
022 * accompanied this code. If applicable, add the following below the
023 * License Header, with the fields enclosed by brackets [] replaced by
024 * your own identifying information:
025 * "Portions Copyrighted [year] [name of copyright owner]"
026 *
027 * Contributor(s):
028 *
029 * The Original Software is NetBeans. The Initial Developer of the Original
030 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
031 * Microsystems, Inc. All Rights Reserved.
032 *
033 * If you wish your version of this file to be governed by only the CDDL
034 * or only the GPL Version 2, indicate your decision by adding
035 * "[Contributor] elects to include this software in this distribution
036 * under the [CDDL or GPL Version 2] license." If you do not indicate a
037 * single choice of license, a recipient has the option to distribute
038 * your version of this file under either the CDDL, the GPL Version 2 or
039 * to extend the choice of license to its licensees as provided above.
040 * However, if you add GPL Version 2 code and therefore, elected the GPL
041 * Version 2 license, then the option applies only if the new code is
042 * made subject to such option by the copyright holder.
043 */
044package jmri.util.xml;
045
046import java.io.CharConversionException;
047import java.io.IOException;
048import java.io.OutputStream;
049import java.io.StringReader;
050import java.util.ArrayList;
051import java.util.Arrays;
052import java.util.HashMap;
053import java.util.HashSet;
054import java.util.List;
055import java.util.Map;
056import java.util.Set;
057import javax.xml.parsers.DocumentBuilder;
058import javax.xml.parsers.DocumentBuilderFactory;
059import javax.xml.parsers.FactoryConfigurationError;
060import javax.xml.parsers.ParserConfigurationException;
061import javax.xml.parsers.SAXParserFactory;
062import javax.xml.transform.OutputKeys;
063import javax.xml.transform.Result;
064import javax.xml.transform.Source;
065import javax.xml.transform.Transformer;
066import javax.xml.transform.TransformerFactory;
067import javax.xml.transform.dom.DOMSource;
068import javax.xml.transform.stream.StreamResult;
069import javax.xml.transform.stream.StreamSource;
070import javax.xml.validation.Schema;
071import javax.xml.validation.Validator;
072import org.slf4j.Logger;
073import org.slf4j.LoggerFactory;
074import org.w3c.dom.Attr;
075import org.w3c.dom.CDATASection;
076import org.w3c.dom.DOMException;
077import org.w3c.dom.DOMImplementation;
078import org.w3c.dom.Document;
079import org.w3c.dom.DocumentType;
080import org.w3c.dom.Element;
081import org.w3c.dom.NamedNodeMap;
082import org.w3c.dom.Node;
083import org.w3c.dom.NodeList;
084import org.w3c.dom.Text;
085import org.xml.sax.EntityResolver;
086import org.xml.sax.ErrorHandler;
087import org.xml.sax.InputSource;
088import org.xml.sax.SAXException;
089import org.xml.sax.SAXParseException;
090import org.xml.sax.XMLReader;
091
092/**
093 * Utility class collecting library methods related to XML processing.
094 *
095 * org.openide.xml.XMLUtil adapted to work in JMRI. This should maintain strict
096 * API conformance to the OpenIDE implementation.
097 */
098public final class XMLUtil extends Object {
099
100    private final static Logger log = LoggerFactory.getLogger(XMLUtil.class);
101
102    /*
103        public static String toCDATA(String val) throws IOException {
104
105        }
106     */
107    private static final char[] DEC2HEX = {
108        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
109    };
110
111    /**
112     * Forbids creating new XMLUtil
113     */
114    private XMLUtil() {
115    }
116
117    // ~~~~~~~~~~~~~~~~~~~~~ SAX related ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118    /**
119     * Create a simple parser.
120     *
121     * @return <code>createXMLReader(false, false)</code>
122     * @throws SAXException if a parser fulfilling given parameters can not be
123     *                      created
124     */
125    public static XMLReader createXMLReader() throws SAXException {
126        return createXMLReader(false, false);
127    }
128
129    /**
130     * Create a simple parser, possibly validating.
131     *
132     * @param validate if true, a validating parser is returned
133     * @return <code>createXMLReader(validate, false)</code>
134     * @throws SAXException if a parser fulfilling given parameters can not be
135     *                      created
136     */
137    public static XMLReader createXMLReader(boolean validate)
138            throws SAXException {
139        return createXMLReader(validate, false);
140    }
141
142    private static SAXParserFactory[][] saxes = new SAXParserFactory[2][2];
143
144    /**
145     * Creates a SAX parser.
146     *
147     * <p>
148     * See {@link #parse} for hints on setting an entity resolver.
149     *
150     * @param validate       if true, a validating parser is returned
151     * @param namespaceAware if true, a namespace aware parser is returned
152     *
153     * @throws FactoryConfigurationError Application developers should never
154     *                                   need to directly catch errors of this
155     *                                   type.
156     * @throws SAXException              if a parser fulfilling given parameters
157     *                                   can not be created
158     *
159     * @return XMLReader configured according to passed parameters
160     */
161    public static synchronized XMLReader createXMLReader(boolean validate, boolean namespaceAware)
162            throws SAXException {
163        SAXParserFactory factory = saxes[validate ? 0 : 1][namespaceAware ? 0 : 1];
164        if (factory == null) {
165            try {
166                factory = SAXParserFactory.newInstance();
167            } catch (FactoryConfigurationError err) {
168                throw err;
169            }
170            factory.setValidating(validate);
171            factory.setNamespaceAware(namespaceAware);
172            saxes[validate ? 0 : 1][namespaceAware ? 0 : 1] = factory;
173        }
174
175        try {
176            return factory.newSAXParser().getXMLReader();
177        } catch (ParserConfigurationException ex) {
178            throw new SAXException("Cannot create parser satisfying configuration parameters", ex); // NOI18N
179        }
180    }
181
182    // ~~~~~~~~~~~~~~~~~~~~~ DOM related ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
183    /**
184     * Creates an empty DOM document. E.g.:
185     * <pre>
186     * Document doc = createDocument("book", null, null, null);
187     * </pre> creates new DOM of a well-formed document with root element named
188     * book.
189     *
190     * @param rootQName       qualified name of root element, for example
191     *                        <code>myroot</code> or <code>ns:myroot</code>
192     * @param namespaceURI    URI of root element namespace or <code>null</code>
193     * @param doctypePublicID public ID of DOCTYPE or <code>null</code>
194     * @param doctypeSystemID system ID of DOCTYPE or <code>null</code> if no
195     *                        DOCTYPE required and doctypePublicID is also
196     *                        <code>null</code>
197     *
198     * @throws DOMException              if new DOM with passed parameters can
199     *                                   not be created
200     * @throws FactoryConfigurationError Application developers should never
201     *                                   need to directly catch errors of this
202     *                                   type.
203     *
204     * @return new DOM Document
205     */
206    public static Document createDocument(
207            String rootQName, String namespaceURI, String doctypePublicID, String doctypeSystemID
208    ) throws DOMException {
209        DOMImplementation impl = getDOMImplementation();
210
211        if ((doctypePublicID != null) && (doctypeSystemID == null)) {
212            throw new IllegalArgumentException("System ID cannot be null if public ID specified. "); // NOI18N
213        }
214
215        DocumentType dtd = null;
216
217        if (doctypeSystemID != null) {
218            dtd = impl.createDocumentType(rootQName, doctypePublicID, doctypeSystemID);
219        }
220
221        return impl.createDocument(namespaceURI, rootQName, dtd);
222    }
223
224    /**
225     * Obtains DOMImpementaton interface providing a number of methods for
226     * performing operations that are independent of any particular DOM
227     * instance.
228     *
229     * @throw DOMException <code>NOT_SUPPORTED_ERR</code> if cannot get
230     * DOMImplementation
231     * @throw FactoryConfigurationError Application developers should never need
232     * to directly catch errors of this type.
233     *
234     * @return DOMImplementation implementation
235     */
236    private static DOMImplementation getDOMImplementation()
237            throws DOMException { //can be made public
238
239        DocumentBuilderFactory factory = getFactory(false, false);
240
241        try {
242            return factory.newDocumentBuilder().getDOMImplementation();
243        } catch (ParserConfigurationException ex) {
244            throw new DOMException(
245                    DOMException.NOT_SUPPORTED_ERR, "Cannot create parser satisfying configuration parameters"
246            ); // NOI18N
247        } catch (RuntimeException e) {
248            // E.g. #36578, IllegalArgumentException. Try to recover gracefully.
249            throw (DOMException) new DOMException(DOMException.NOT_SUPPORTED_ERR, e.toString()).initCause(e);
250        }
251    }
252
253    private static DocumentBuilderFactory[][] doms = new DocumentBuilderFactory[2][2];
254
255    private static synchronized DocumentBuilderFactory getFactory(boolean validate, boolean namespaceAware) {
256        DocumentBuilderFactory factory = doms[validate ? 0 : 1][namespaceAware ? 0 : 1];
257        if (factory == null) {
258            factory = DocumentBuilderFactory.newInstance();
259            factory.setValidating(validate);
260            factory.setNamespaceAware(namespaceAware);
261            doms[validate ? 0 : 1][namespaceAware ? 0 : 1] = factory;
262        }
263        return factory;
264    }
265
266    /**
267     * Parses an XML document into a DOM tree.
268     *
269     * <div class="nonnormative">
270     *
271     * <p>
272     * Remember that when parsing XML files you often want to set an explicit
273     * entity resolver. For example, consider a file such as this:
274     *
275     * <pre>
276     * &lt;?xml version="1.0" encoding="UTF-8"?&gt;
277     * &lt;!DOCTYPE root PUBLIC "-//NetBeans//DTD Foo 1.0//EN" "http://www.netbeans.org/dtds/foo-1_0.dtd"&gt;
278     * &lt;root/&gt;
279     * </pre>
280     *
281     * <p>
282     * If you parse this with a null entity resolver, or you use the default
283     * resolver (EntityCatalog.getDefault) but do not do anything special with
284     * this DTD, you will probably find the parse blocking to make a network
285     * connection <em>even when you are not validating</em>. That is because
286     * DTDs can be used to define entities and other XML oddities, and are not a
287     * pure constraint language like Schema or RELAX-NG.
288     * <p>
289     * There are three basic ways to avoid the network connection.
290     *
291     * <ol>
292
293     * <li>
294     * Register the DTD. This is generally the best thing to do. See
295     * EntityCatalog's documentation for details, but for example in your layer
296     * use:
297     * <br>
298     * <pre>
299     * &lt;filesystem&gt;
300     *   &lt;folder name="xml"&gt;
301     *     &lt;folder name="entities"&gt;
302     *       &lt;folder name="NetBeans"&gt;
303     *         &lt;file name="DTD_Foo_1_0"
304     *               url="resources/foo-1_0.dtd"&gt;
305     *           &lt;attr name="hint.originalPublicID"
306     *                 stringvalue="-//NetBeans//DTD Foo 1.0//EN"/&gt;
307     *         &lt;/file&gt;
308     *       &lt;/folder&gt;
309     *     &lt;/folder&gt;
310     *   &lt;/folder&gt;
311     * &lt;/filesystem&gt;
312     * </pre>
313     *
314     * <p>
315     * Now the default system entity catalog will resolve the public ID to the
316     * local copy in your module, not the network copy. Additionally, anyone who
317     * mounts the "NetBeans Catalog" in the XML Entity Catalogs node in the
318     * Runtime tab will be able to use your local copy of the DTD automatically,
319     * for validation, code completion, etc. (The network URL should really
320     * exist, though, for the benefit of other tools!)</li>
321     *
322     * <li>
323     * You can also set an explicit entity resolver which maps that particular
324     * public ID to some local copy of the DTD, if you do not want to register
325     * it globally in the system for some reason. If handed other public IDs,
326     * just return null to indicate that the system ID should be
327     * loaded.</li>
328     *
329     * <li>
330     * In some cases where XML parsing is very performance-sensitive, and you
331     * know that you do not need validation and furthermore that the DTD defines
332     * no infoset (there are no entity or character definitions, etc.), you can
333     * speed up the parse. Turn off validation, but also supply a custom entity
334     * resolver that does not even bother to load the DTD at all:<br>
335     *
336     * <pre>
337     * public InputSource resolveEntity(String pubid, String sysid)
338     *     throws SAXException, IOException {
339     *   if (pubid.equals("-//NetBeans//DTD Foo 1.0//EN")) {
340     *     return new InputSource(new ByteArrayInputStream(new byte[0]));
341     *   } else {
342     *     return EntityCatalog.getDefault().resolveEntity(pubid, sysid);
343     *   }
344     * }
345     * </pre></li>
346     *
347     * </ol>
348     *
349     * </div>
350     *
351     * @param input          a parser input (for URL users use:
352     *                       <code>new InputSource(url.toString())</code>
353     * @param validate       if true validating parser is used
354     * @param namespaceAware if true DOM is created by namespace aware parser
355     * @param errorHandler   a error handler to notify about exception (such as
356     *                       {@link #defaultErrorHandler}) or <code>null</code>
357     * @param entityResolver SAX entity resolver (such as
358     *                       EntityCatalog#getDefault) or <code>null</code>
359     *
360     * @throws IOException               if an I/O problem during parsing occurs
361     * @throws SAXException              is thrown if a parser error occurs
362     * @throws FactoryConfigurationError Application developers should never
363     *                                   need to directly catch errors of this
364     *                                   type.
365     *
366     * @return document representing given input
367     */
368    public static Document parse(
369            InputSource input, boolean validate, boolean namespaceAware, ErrorHandler errorHandler,
370            EntityResolver entityResolver
371    ) throws IOException, SAXException {
372
373        DocumentBuilder builder = null;
374        DocumentBuilderFactory factory = getFactory(validate, namespaceAware);
375
376        try {
377            builder = factory.newDocumentBuilder();
378        } catch (ParserConfigurationException ex) {
379            throw new SAXException("Cannot create parser satisfying configuration parameters", ex); // NOI18N
380        }
381
382        if (errorHandler != null) {
383            builder.setErrorHandler(errorHandler);
384        }
385
386        if (entityResolver != null) {
387            builder.setEntityResolver(entityResolver);
388        }
389
390        return builder.parse(input);
391    }
392
393    /**
394     * Identity transformation in XSLT with indentation added. Just using the
395     * identity transform and calling t.setOutputProperty(OutputKeys.INDENT,
396     * "yes"); t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount",
397     * "4"); does not work currently. You really have to use this bogus
398     * stylesheet.
399     *
400     * @see "JDK bug #5064280"
401     */
402    private static final String IDENTITY_XSLT_WITH_INDENT
403            = "<xsl:stylesheet version='1.0' "
404            + // NOI18N
405            "xmlns:xsl='http://www.w3.org/1999/XSL/Transform' "
406            + // NOI18N
407            "xmlns:xalan='http://xml.apache.org/xslt' "
408            + // NOI18N
409            "exclude-result-prefixes='xalan'>"
410            + // NOI18N
411            "<xsl:output method='xml' indent='yes' xalan:indent-amount='4'/>"
412            + // NOI18N
413            "<xsl:template match='@*|node()'>"
414            + // NOI18N
415            "<xsl:copy>"
416            + // NOI18N
417            "<xsl:apply-templates select='@*|node()'/>"
418            + // NOI18N
419            "</xsl:copy>"
420            + // NOI18N
421            "</xsl:template>"
422            + // NOI18N
423            "</xsl:stylesheet>"; // NOI18N
424    /**
425     * Workaround for JAXP bug 7150637 / XALANJ-1497.
426     */
427    private static final String ORACLE_IS_STANDALONE = "http://www.oracle.com/xml/is-standalone";
428
429    /**
430     * Writes a DOM document to a stream. The precise output format is not
431     * guaranteed but this method will attempt to indent it sensibly.
432     *
433     * <p class="nonnormative"><b>Important</b>: There might be some problems
434     * with <code>&lt;![CDATA[ ]]&gt;</code> sections in the DOM tree you pass
435     * into this method. Specifically, some CDATA sections my not be written as
436     * CDATA section or may be merged with other CDATA section at the same
437     * level. Also if plain text nodes are mixed with CDATA sections at the same
438     * level all text is likely to end up in one big CDATA section.
439     * <br>
440     * For nodes that only have one CDATA section this method should work fine.
441     *
442     * @param doc DOM document to be written
443     * @param out data sink
444     * @param enc XML-defined encoding name (for example, "UTF-8")
445     * @throws IOException if JAXP fails or the stream cannot be written to
446     */
447    public static void write(Document doc, OutputStream out, String enc) throws IOException {
448        if (enc == null) {
449            throw new NullPointerException("You must set an encoding; use \"UTF-8\" unless you have a good reason not to!"); // NOI18N
450        }
451        Document doc2 = normalize(doc);
452        try {
453            TransformerFactory tf = TransformerFactory.newInstance();
454            Transformer t = tf.newTransformer(
455                    new StreamSource(new StringReader(IDENTITY_XSLT_WITH_INDENT)));
456            DocumentType dt = doc2.getDoctype();
457            if (dt != null) {
458                String pub = dt.getPublicId();
459                if (pub != null) {
460                    t.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, pub);
461                }
462                String sys = dt.getSystemId();
463                if (sys != null) {
464                    t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, sys);
465                }
466            }
467            t.setOutputProperty(OutputKeys.ENCODING, enc);
468            try {
469                t.setOutputProperty(ORACLE_IS_STANDALONE, "yes");
470            } catch (IllegalArgumentException x) {
471                // fine, introduced in JDK 7u4
472            }
473
474            // See #123816
475            Set<String> cdataQNames = new HashSet<String>();
476            collectCDATASections(doc2, cdataQNames);
477            if (cdataQNames.size() > 0) {
478                StringBuilder cdataSections = new StringBuilder();
479                for (String s : cdataQNames) {
480                    cdataSections.append(s).append(' '); // NOI18N
481                }
482                t.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, cdataSections.toString());
483            }
484
485            Source source = new DOMSource(doc2);
486            Result result = new StreamResult(out);
487            t.transform(source, result);
488        } catch (javax.xml.transform.TransformerException | RuntimeException e) { // catch anything that happens
489            throw new IOException(e);
490        }
491    }
492
493    private static void collectCDATASections(Node node, Set<String> cdataQNames) {
494        if (node instanceof CDATASection) {
495            Node parent = node.getParentNode();
496            if (parent != null) {
497                String uri = parent.getNamespaceURI();
498                if (uri != null) {
499                    cdataQNames.add("{" + uri + "}" + parent.getNodeName()); // NOI18N
500                } else {
501                    cdataQNames.add(parent.getNodeName());
502                }
503            }
504        }
505
506        NodeList children = node.getChildNodes();
507        for (int i = 0; i < children.getLength(); i++) {
508            collectCDATASections(children.item(i), cdataQNames);
509        }
510    }
511
512    /**
513     * Check whether a DOM tree is valid according to a schema. Example of
514     * usage:
515     * <pre>
516     * Element fragment = ...;
517     * SchemaFactory f = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
518     * Schema s = f.newSchema(This.class.getResource("something.xsd"));
519     * try {
520     *     XMLUtil.validate(fragment, s);
521     *     // valid
522     * } catch (SAXException x) {
523     *     // invalid
524     * }
525     * </pre>
526     *
527     * @param data   a DOM tree
528     * @param schema a parsed schema
529     * @throws SAXException if validation failed
530     * @since org.openide.util 7.17
531     */
532    public static void validate(Element data, Schema schema) throws SAXException {
533        Validator v = schema.newValidator();
534        final SAXException[] error = {null};
535        v.setErrorHandler(new ErrorHandler() {
536            @Override
537            public void warning(SAXParseException x) throws SAXException {
538            }
539
540            @Override
541            public void error(SAXParseException x) throws SAXException {
542                // Just rethrowing it is bad because it will also print it to stderr.
543                error[0] = x;
544            }
545
546            @Override
547            public void fatalError(SAXParseException x) throws SAXException {
548                error[0] = x;
549            }
550        });
551        try {
552            v.validate(new DOMSource(fixupAttrs(data)));
553        } catch (IOException x) {
554            assert false : x;
555        }
556        if (error[0] != null) {
557            throw error[0];
558        }
559    }
560
561    private static Element fixupAttrs(Element root) { // #140905
562        // #6529766/#6531160: some versions of JAXP reject attributes set using setAttribute
563        // (rather than setAttributeNS) even though the schema calls for no-NS attrs!
564        // JDK 5 is fine; JDK 6 broken; JDK 6u2+ fixed
565        // #146081: xml:base attributes mess up validation too.
566        Element copy = (Element) root.cloneNode(true);
567        fixupAttrsSingle(copy);
568        NodeList nl = copy.getElementsByTagName("*"); // NOI18N
569        for (int i = 0; i < nl.getLength(); i++) {
570            fixupAttrsSingle((Element) nl.item(i));
571        }
572        return copy;
573    }
574
575    private static void fixupAttrsSingle(Element e) throws DOMException {
576        removeXmlBase(e);
577        Map<String, String> replace = new HashMap<String, String>();
578        NamedNodeMap attrs = e.getAttributes();
579        for (int j = 0; j < attrs.getLength(); j++) {
580            Attr attr = (Attr) attrs.item(j);
581            if (attr.getNamespaceURI() == null && !attr.getName().equals("xmlns")) { // NOI18N
582                replace.put(attr.getName(), attr.getValue());
583            }
584        }
585        for (Map.Entry<String, String> entry : replace.entrySet()) {
586            e.removeAttribute(entry.getKey());
587            e.setAttributeNS(null, entry.getKey(), entry.getValue());
588        }
589    }
590
591    private static void removeXmlBase(Element e) {
592        e.removeAttributeNS("http://www.w3.org/XML/1998/namespace", "base"); // NOI18N
593        e.removeAttribute("xml:base"); // NOI18N
594    }
595
596    /**
597     * Escape passed string as XML attibute value (<code>&lt;</code>,
598     * <code>&amp;</code>, <code>'</code> and <code>"</code> will be escaped.
599     * Note: An XML processor returns normalized value that can be different.
600     *
601     * @param val a string to be escaped
602     *
603     * @return escaped value
604     * @throws CharConversionException if val contains an improper XML character
605     *
606     * @since 1.40
607     */
608    public static String toAttributeValue(String val) throws CharConversionException {
609        if (val == null) {
610            throw new CharConversionException("null"); // NOI18N
611        }
612
613        if (checkAttributeCharacters(val)) {
614            return val;
615        }
616
617        StringBuilder buf = new StringBuilder();
618
619        for (int i = 0; i < val.length(); i++) {
620            char ch = val.charAt(i);
621
622            if ('<' == ch) {
623                buf.append("&lt;");
624
625                continue;
626            } else if ('&' == ch) {
627                buf.append("&amp;");
628
629                continue;
630            } else if ('\'' == ch) {
631                buf.append("&apos;");
632
633                continue;
634            } else if ('"' == ch) {
635                buf.append("&quot;");
636
637                continue;
638            }
639
640            buf.append(ch);
641        }
642
643        return buf.toString();
644    }
645
646    /**
647     * Escape passed string as XML element content (<code>&lt;</code>,
648     * <code>&amp;</code> and <code>&gt;</code> in <code>]]&gt;</code>
649     * sequences).
650     *
651     * @param val a string to be escaped
652     *
653     * @return escaped value
654     * @throws CharConversionException if val contains an improper XML character
655     *
656     * @since 1.40
657     */
658    public static String toElementContent(String val) throws CharConversionException {
659        if (val == null) {
660            throw new CharConversionException("null"); // NOI18N
661        }
662
663        if (checkContentCharacters(val)) {
664            return val;
665        }
666
667        StringBuilder buf = new StringBuilder();
668
669        for (int i = 0; i < val.length(); i++) {
670            char ch = val.charAt(i);
671
672            if ('<' == ch) {
673                buf.append("&lt;");
674
675                continue;
676            } else if ('&' == ch) {
677                buf.append("&amp;");
678
679                continue;
680            } else if (('>' == ch) && (i > 1) && (val.charAt(i - 2) == ']') && (val.charAt(i - 1) == ']')) {
681                buf.append("&gt;");
682
683                continue;
684            }
685
686            buf.append(ch);
687        }
688
689        return buf.toString();
690    }
691
692    /**
693     * Can be used to encode values that contain invalid XML characters. At SAX
694     * parser end must be used pair method to get original value.
695     *
696     * @param val   data to be converted
697     * @param start offset
698     * @param len   count
699     * @return the converted data
700     *
701     * @since 1.29
702     */
703    public static String toHex(byte[] val, int start, int len) {
704        StringBuilder buf = new StringBuilder();
705
706        for (int i = 0; i < len; i++) {
707            byte b = val[start + i];
708            buf.append(DEC2HEX[(b & 0xf0) >> 4]);
709            buf.append(DEC2HEX[b & 0x0f]);
710        }
711
712        return buf.toString();
713    }
714
715    /**
716     * Decodes data encoded using {@link #toHex(byte[],int,int) toHex}.
717     *
718     * @param hex   data to be converted
719     * @param start offset
720     * @param len   count
721     * @return the converted data
722     *
723     * @throws IOException if input does not represent hex encoded value
724     *
725     * @since 1.29
726     */
727    public static byte[] fromHex(char[] hex, int start, int len)
728            throws IOException {
729        if (hex == null) {
730            throw new IOException("null");
731        }
732
733        int i = hex.length;
734
735        if ((i % 2) != 0) {
736            throw new IOException("odd length");
737        }
738
739        byte[] magic = new byte[i / 2];
740
741        for (; i > 0; i -= 2) {
742            String g = new String(hex, i - 2, 2);
743
744            try {
745                magic[(i / 2) - 1] = (byte) Integer.parseInt(g, 16);
746            } catch (NumberFormatException ex) {
747                throw new IOException(ex.getLocalizedMessage());
748            }
749        }
750
751        return magic;
752    }
753
754    /**
755     * Check if all passed characters match XML expression [2].
756     *
757     * @return true if no escaping necessary
758     * @throws CharConversionException if contains invalid chars
759     */
760    private static boolean checkAttributeCharacters(String chars)
761            throws CharConversionException {
762        boolean escape = false;
763
764        for (int i = 0; i < chars.length(); i++) {
765            char ch = chars.charAt(i);
766
767            if (ch <= 93) { // we are UNICODE ']'
768
769                switch (ch) {
770                    case 0x9:
771                    case 0xA:
772                    case 0xD:
773
774                        continue;
775
776                    case '\'':
777                    case '"':
778                    case '<':
779                    case '&':
780                        escape = true;
781
782                        continue;
783
784                    default:
785
786                        if (ch < 0x20) {
787                            throw new CharConversionException("Invalid XML character &#" + ((int) ch) + ";.");
788                        }
789                }
790            }
791        }
792
793        return escape == false;
794    }
795
796    /**
797     * Check if all passed characters match XML expression [2].
798     *
799     * @return true if no escaping necessary
800     * @throws CharConversionException if contains invalid chars
801     */
802    private static boolean checkContentCharacters(String chars)
803            throws CharConversionException {
804        boolean escape = false;
805
806        for (int i = 0; i < chars.length(); i++) {
807            char ch = chars.charAt(i);
808
809            if (ch <= 93) { // we are UNICODE ']'
810
811                switch (ch) {
812                    case 0x9:
813                    case 0xA:
814                    case 0xD:
815
816                        continue;
817
818                    case '>': // only ]]> is dangerous
819
820                        if (escape) {
821                            continue;
822                        }
823
824                        escape = (i > 0) && (chars.charAt(i - 1) == ']');
825
826                        continue;
827
828                    case '<':
829                    case '&':
830                        escape = true;
831
832                        continue;
833
834                    default:
835
836                        if (ch < 0x20) {
837                            throw new CharConversionException("Invalid XML character &#" + ((int) ch) + ";.");
838                        }
839                }
840            }
841        }
842
843        return escape == false;
844    }
845
846    /**
847     * Try to normalize a document by removing nonsignificant whitespace.
848     *
849     * @see "#62006"
850     */
851    private static Document normalize(Document orig) throws IOException {
852        DocumentBuilder builder = null;
853        DocumentBuilderFactory factory = getFactory(false, false);
854        try {
855            builder = factory.newDocumentBuilder();
856        } catch (ParserConfigurationException e) {
857            throw new IOException("Cannot create parser satisfying configuration parameters: " + e, e); // NOI18N
858        }
859
860        DocumentType doctype = null;
861        NodeList nl = orig.getChildNodes();
862        for (int i = 0; i < nl.getLength(); i++) {
863            if (nl.item(i) instanceof DocumentType) {
864                // We cannot import DocumentType's, so we need to manually copy it.
865                doctype = (DocumentType) nl.item(i);
866            }
867        }
868        Document doc;
869        if (doctype != null) {
870            doc = builder.getDOMImplementation().createDocument(
871                    orig.getDocumentElement().getNamespaceURI(),
872                    orig.getDocumentElement().getTagName(),
873                    builder.getDOMImplementation().createDocumentType(
874                            orig.getDoctype().getName(),
875                            orig.getDoctype().getPublicId(),
876                            orig.getDoctype().getSystemId()));
877            // XXX what about entity decls inside the DOCTYPE?
878            doc.removeChild(doc.getDocumentElement());
879        } else {
880            doc = builder.newDocument();
881        }
882        for (int i = 0; i < nl.getLength(); i++) {
883            Node node = nl.item(i);
884            if (!(node instanceof DocumentType)) {
885                try {
886                    doc.appendChild(doc.importNode(node, true));
887                } catch (DOMException x) {
888                    // Thrown in NB-Core-Build #2896 & 2898 inside GeneratedFilesHelper.applyBuildExtensions
889                    throw new IOException("Could not import or append " + node + " of " + node.getClass(), x);
890                }
891            }
892        }
893        doc.normalize();
894        nl = doc.getElementsByTagName("*"); // NOI18N
895        for (int i = 0; i < nl.getLength(); i++) {
896            Element e = (Element) nl.item(i);
897            removeXmlBase(e);
898            NodeList nl2 = e.getChildNodes();
899            for (int j = 0; j < nl2.getLength(); j++) {
900                Node n = nl2.item(j);
901                if (n instanceof Text && ((Text) n).getNodeValue().trim().length() == 0) {
902                    e.removeChild(n);
903                    j--; // since list is dynamic
904                }
905            }
906        }
907        return doc;
908    }
909
910    /**
911     * Append a child element to the parent at the specified location.
912     *
913     * Starting with a valid document, append an element according to the schema
914     * sequence represented by the <code>order</code>. All existing child
915     * elements must be include as well as the new element. The existing child
916     * element following the new child is important, as the element will be
917     * 'inserted before', not 'inserted after'.
918     *
919     * @param parent parent to which the child will be appended
920     * @param el     element to be added
921     * @param order  order of the elements which must be followed
922     * @throws IllegalArgumentException if the order cannot be followed, either
923     *                                  a missing existing or new child element
924     *                                  is not specified in order
925     *
926     * @since 8.4
927     */
928    public static void appendChildElement(Element parent, Element el, String[] order) throws IllegalArgumentException {
929        List<String> l = Arrays.asList(order);
930        int index = l.indexOf(el.getLocalName());
931
932        // ensure the new new element is contained in the 'order'
933        if (index == -1) {
934            throw new IllegalArgumentException("new child element '" + el.getLocalName() + "' not specified in order " + l); // NOI18N
935        }
936
937        List<Element> elements = findSubElements(parent);
938        Element insertBefore = null;
939
940        for (Element e : elements) {
941            int index2 = l.indexOf(e.getLocalName());
942            // ensure that all existing elements are in 'order'
943            if (index2 == -1) {
944                throw new IllegalArgumentException("Existing child element '" + e.getLocalName() + "' not specified in order " + l);  // NOI18N
945            }
946            if (index2 > index) {
947                insertBefore = e;
948                break;
949            }
950        }
951
952        parent.insertBefore(el, insertBefore);
953    }
954
955    /**
956     * Find all direct child elements of an element. Children which are
957     * all-whitespace text nodes or comments are ignored; others cause an
958     * exception to be thrown.
959     *
960     * @param parent a parent element in a DOM tree
961     * @return a list of direct child elements (may be empty)
962     * @throws IllegalArgumentException if there are non-element children
963     *                                  besides whitespace
964     *
965     * @since 8.4
966     */
967    public static List<Element> findSubElements(Element parent) throws IllegalArgumentException {
968        NodeList l = parent.getChildNodes();
969        List<Element> elements = new ArrayList<Element>(l.getLength());
970        for (int i = 0; i < l.getLength(); i++) {
971            Node n = l.item(i);
972            if (n.getNodeType() == Node.ELEMENT_NODE) {
973                elements.add((Element) n);
974            } else if (n.getNodeType() == Node.TEXT_NODE) {
975                String text = ((Text) n).getNodeValue();
976                if (text.trim().length() > 0) {
977                    throw new IllegalArgumentException("non-ws text encountered in " + parent + ": " + text); // NOI18N
978                }
979            } else if (n.getNodeType() == Node.COMMENT_NODE) {
980                // OK, ignore
981            } else {
982                throw new IllegalArgumentException("unexpected non-element child of " + parent + ": " + n); // NOI18N
983            }
984        }
985        return elements;
986    }
987
988    /**
989     * Search for an XML element in the direct children of parent only.
990     *
991     * This compares localName (nodeName if localName is null) to name, and
992     * checks the tags namespace with the provided namespace. A
993     * <code>null</code> namespace will match any namespace.
994     * <p>
995     * This is differs from the DOM version by:
996     * <ul>
997     * <li>not searching recursively</li>
998     * <li>returns a single result</li>
999     * </ul>
1000     *
1001     * @param parent    a parent element
1002     * @param name      the intended local name
1003     * @param namespace the intended namespace (or null)
1004     * @return the one child element with that name, or null if none
1005     * @throws IllegalArgumentException if there is multiple elements of the
1006     *                                  same name
1007     *
1008     * @since 8.4
1009     */
1010    public static Element findElement(Element parent, String name, String namespace) throws IllegalArgumentException {
1011        Element result = null;
1012        NodeList l = parent.getChildNodes();
1013        int nodeCount = l.getLength();
1014        for (int i = 0; i < nodeCount; i++) {
1015            if (l.item(i).getNodeType() == Node.ELEMENT_NODE) {
1016                Node node = l.item(i);
1017                String localName = node.getLocalName();
1018                localName = localName == null ? node.getNodeName() : localName;
1019
1020                if (name.equals(localName)
1021                        && (namespace == null || namespace.equals(node.getNamespaceURI()))) {
1022                    if (result == null) {
1023                        result = (Element) node;
1024                    } else {
1025                        throw new IllegalArgumentException("more than one element with same name found");
1026                    }
1027                }
1028            }
1029        }
1030        return result;
1031    }
1032
1033    /**
1034     * Extract nested text from a node. Currently does not handle coalescing
1035     * text nodes, CDATA sections, etc.
1036     *
1037     * @param parent a parent element
1038     * @return the nested text, or null if none was found
1039     *
1040     * @since 8.4
1041     */
1042    public static String findText(Node parent) {
1043        NodeList l = parent.getChildNodes();
1044        for (int i = 0; i < l.getLength(); i++) {
1045            if (l.item(i).getNodeType() == Node.TEXT_NODE) {
1046                Text text = (Text) l.item(i);
1047                return text.getNodeValue();
1048            }
1049        }
1050        return null;
1051    }
1052
1053    /**
1054     * Convert an XML fragment from one namespace to another.
1055     *
1056     * @param from      element to translate
1057     * @param namespace namespace to be translated to
1058     * @return the element in the new namespace
1059     *
1060     * @since 8.4
1061     */
1062    public static Element translateXML(Element from, String namespace) {
1063        Element to = from.getOwnerDocument().createElementNS(namespace, from.getLocalName());
1064        NodeList nl = from.getChildNodes();
1065        int length = nl.getLength();
1066        for (int i = 0; i < length; i++) {
1067            Node node = nl.item(i);
1068            Node newNode;
1069            if (node.getNodeType() == Node.ELEMENT_NODE) {
1070                newNode = translateXML((Element) node, namespace);
1071            } else {
1072                newNode = node.cloneNode(true);
1073            }
1074            to.appendChild(newNode);
1075        }
1076        NamedNodeMap m = from.getAttributes();
1077        for (int i = 0; i < m.getLength(); i++) {
1078            Node attr = m.item(i);
1079            to.setAttribute(attr.getNodeName(), attr.getNodeValue());
1080        }
1081        return to;
1082    }
1083
1084    /**
1085     * Copy elements from one document to another attaching at the specified
1086     * element and translating the namespace.
1087     *
1088     * @param from         copy the children of this element (exclusive)
1089     * @param to           where to attach the copied elements
1090     * @param newNamespace destination namespace
1091     *
1092     * @since 8.4
1093     */
1094    public static void copyDocument(Element from, Element to, String newNamespace) {
1095        Document doc = to.getOwnerDocument();
1096        NodeList nl = from.getChildNodes();
1097        int length = nl.getLength();
1098        for (int i = 0; i < length; i++) {
1099            Node node = nl.item(i);
1100            Node newNode = null;
1101            if (Node.ELEMENT_NODE == node.getNodeType()) {
1102                Element oldElement = (Element) node;
1103                newNode = doc.createElementNS(newNamespace, oldElement.getTagName());
1104                NamedNodeMap m = oldElement.getAttributes();
1105                Element newElement = (Element) newNode;
1106                for (int index = 0; index < m.getLength(); index++) {
1107                    Node attr = m.item(index);
1108                    newElement.setAttribute(attr.getNodeName(), attr.getNodeValue());
1109                }
1110                copyDocument(oldElement, newElement, newNamespace);
1111            } else {
1112                newNode = node.cloneNode(true);
1113                newNode = to.getOwnerDocument().importNode(newNode, true);
1114            }
1115            if (newNode != null) {
1116                to.appendChild(newNode);
1117            }
1118        }
1119    }
1120
1121    /**
1122     * Create an XML error handler that rethrows errors and fatal errors and
1123     * logs warnings.
1124     *
1125     * @return a standard error handler
1126     *
1127     * @since 8.4
1128     */
1129    public static ErrorHandler defaultErrorHandler() {
1130        return new ErrHandler();
1131    }
1132
1133    private static final class ErrHandler implements ErrorHandler {
1134
1135        ErrHandler() {
1136        }
1137
1138        private void annotate(SAXParseException exception) {
1139            log.error("SAXParseException", exception);
1140        }
1141
1142        @Override
1143        public void fatalError(SAXParseException exception) throws SAXException {
1144            annotate(exception);
1145            throw exception;
1146        }
1147
1148        @Override
1149        public void error(SAXParseException exception) throws SAXException {
1150            annotate(exception);
1151            throw exception;
1152        }
1153
1154        @Override
1155        public void warning(SAXParseException exception) throws SAXException {
1156            log.warn("SAXParseException", exception);
1157        }
1158
1159    }
1160}