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 * <?xml version="1.0" encoding="UTF-8"?> 277 * <!DOCTYPE root PUBLIC "-//NetBeans//DTD Foo 1.0//EN" "http://www.netbeans.org/dtds/foo-1_0.dtd"> 278 * <root/> 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 * <filesystem> 300 * <folder name="xml"> 301 * <folder name="entities"> 302 * <folder name="NetBeans"> 303 * <file name="DTD_Foo_1_0" 304 * url="resources/foo-1_0.dtd"> 305 * <attr name="hint.originalPublicID" 306 * stringvalue="-//NetBeans//DTD Foo 1.0//EN"/> 307 * </file> 308 * </folder> 309 * </folder> 310 * </folder> 311 * </filesystem> 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><![CDATA[ ]]></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><</code>, 598 * <code>&</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("<"); 624 625 continue; 626 } else if ('&' == ch) { 627 buf.append("&"); 628 629 continue; 630 } else if ('\'' == ch) { 631 buf.append("'"); 632 633 continue; 634 } else if ('"' == ch) { 635 buf.append("""); 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><</code>, 648 * <code>&</code> and <code>></code> in <code>]]></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("<"); 674 675 continue; 676 } else if ('&' == ch) { 677 buf.append("&"); 678 679 continue; 680 } else if (('>' == ch) && (i > 1) && (val.charAt(i - 2) == ']') && (val.charAt(i - 1) == ']')) { 681 buf.append(">"); 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}