001package org.jsoup.helper; 002 003import org.jsoup.internal.NamespaceBindings; 004import org.jsoup.internal.StringUtil; 005import org.jsoup.nodes.Attribute; 006import org.jsoup.nodes.Attributes; 007import org.jsoup.nodes.CDataNode; 008import org.jsoup.nodes.Comment; 009import org.jsoup.nodes.DataNode; 010import org.jsoup.nodes.TextNode; 011import org.jsoup.nodes.XmlDeclaration; 012import org.jsoup.parser.Parser; 013import org.jsoup.select.NodeVisitor; 014import org.jsoup.select.Selector; 015import org.w3c.dom.DOMException; 016import org.w3c.dom.Document; 017import org.w3c.dom.DocumentType; 018import org.w3c.dom.Element; 019import org.w3c.dom.Node; 020import org.w3c.dom.NodeList; 021import org.jspecify.annotations.Nullable; 022 023import javax.xml.XMLConstants; 024import javax.xml.parsers.DocumentBuilder; 025import javax.xml.parsers.DocumentBuilderFactory; 026import javax.xml.parsers.ParserConfigurationException; 027import javax.xml.transform.OutputKeys; 028import javax.xml.transform.Transformer; 029import javax.xml.transform.TransformerException; 030import javax.xml.transform.TransformerFactory; 031import javax.xml.transform.dom.DOMSource; 032import javax.xml.transform.stream.StreamResult; 033import javax.xml.xpath.XPathConstants; 034import javax.xml.xpath.XPathExpression; 035import javax.xml.xpath.XPathExpressionException; 036import javax.xml.xpath.XPathFactory; 037import javax.xml.xpath.XPathFactoryConfigurationException; 038import java.io.StringWriter; 039import java.util.ArrayList; 040import java.util.HashMap; 041import java.util.List; 042import java.util.Map; 043import java.util.Properties; 044import java.util.regex.Matcher; 045import java.util.regex.Pattern; 046 047import static javax.xml.transform.OutputKeys.METHOD; 048import static org.jsoup.nodes.Document.OutputSettings.Syntax; 049 050/** 051 * Helper class to transform a {@link org.jsoup.nodes.Document} to a {@link org.w3c.dom.Document org.w3c.dom.Document}, 052 * for integration with toolsets that use the W3C DOM. 053 */ 054public class W3CDom { 055 /** For W3C Documents created by this class, this property is set on each node to link back to the original jsoup node. */ 056 public static final String SourceProperty = "jsoupSource"; 057 private static final String ContextProperty = "jsoupContextSource"; // tracks the jsoup context element on w3c doc 058 private static final String ContextNodeProperty = "jsoupContextNode"; // the w3c node used as the creating context 059 060 /** 061 To get support for XPath versions > 1, set this property to the classname of an alternate XPathFactory 062 implementation. (For e.g. {@code net.sf.saxon.xpath.XPathFactoryImpl}). 063 */ 064 public static final String XPathFactoryProperty = "javax.xml.xpath.XPathFactory:jsoup"; 065 066 protected DocumentBuilderFactory factory; 067 private boolean namespaceAware = true; // false when using selectXpath, for user's query convenience 068 069 public W3CDom() { 070 factory = DocumentBuilderFactory.newInstance(); 071 factory.setNamespaceAware(true); 072 } 073 074 /** 075 Returns if this W3C DOM is namespace aware. By default, this will be {@code true}, but is disabled for simplicity 076 when using XPath selectors in {@link org.jsoup.nodes.Element#selectXpath(String)}. 077 @return the current namespace aware setting. 078 */ 079 public boolean namespaceAware() { 080 return namespaceAware; 081 } 082 083 /** 084 Update the namespace aware setting. This impacts the factory that is used to create W3C nodes from jsoup nodes. 085 <p>For HTML documents, controls if the document will be in the default {@code http://www.w3.org/1999/xhtml} 086 namespace if otherwise unset.</p>. 087 @param namespaceAware the updated setting 088 @return this W3CDom, for chaining. 089 */ 090 public W3CDom namespaceAware(boolean namespaceAware) { 091 this.namespaceAware = namespaceAware; 092 factory.setNamespaceAware(namespaceAware); 093 return this; 094 } 095 096 /** 097 * Converts a jsoup DOM to a W3C DOM. 098 * 099 * @param in jsoup Document 100 * @return W3C Document 101 */ 102 public static Document convert(org.jsoup.nodes.Document in) { 103 return (new W3CDom().fromJsoup(in)); 104 } 105 106 /** 107 * Serialize a W3C document to a String. Provide Properties to define output settings including if HTML or XML. If 108 * you don't provide the properties ({@code null}), the output will be auto-detected based on the content of the 109 * document. 110 * 111 * @param doc Document 112 * @param properties (optional/nullable) the output properties to use. See {@link 113 * Transformer#setOutputProperties(Properties)} and {@link OutputKeys} 114 * @return Document as string 115 * @see #OutputHtml 116 * @see #OutputXml 117 * @see OutputKeys#ENCODING 118 * @see OutputKeys#OMIT_XML_DECLARATION 119 * @see OutputKeys#STANDALONE 120 * @see OutputKeys#DOCTYPE_PUBLIC 121 * @see OutputKeys#CDATA_SECTION_ELEMENTS 122 * @see OutputKeys#INDENT 123 * @see OutputKeys#MEDIA_TYPE 124 */ 125 public static String asString(Document doc, @Nullable Map<String, String> properties) { 126 try { 127 DOMSource domSource = new DOMSource(doc); 128 StringWriter writer = new StringWriter(); 129 StreamResult result = new StreamResult(writer); 130 TransformerFactory tf = TransformerFactory.newInstance(); 131 Transformer transformer = tf.newTransformer(); 132 if (properties != null) 133 transformer.setOutputProperties(propertiesFromMap(properties)); 134 135 if (doc.getDoctype() != null) { 136 DocumentType doctype = doc.getDoctype(); 137 if (!StringUtil.isBlank(doctype.getPublicId())) 138 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doctype.getPublicId()); 139 if (!StringUtil.isBlank(doctype.getSystemId())) 140 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doctype.getSystemId()); 141 // handle <!doctype html> for legacy dom. 142 else if (doctype.getName().equalsIgnoreCase("html") 143 && StringUtil.isBlank(doctype.getPublicId()) 144 && StringUtil.isBlank(doctype.getSystemId())) 145 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat"); 146 } 147 148 transformer.transform(domSource, result); 149 return writer.toString(); 150 151 } catch (TransformerException e) { 152 throw new IllegalStateException(e); 153 } 154 } 155 156 static Properties propertiesFromMap(Map<String, String> map) { 157 Properties props = new Properties(); 158 props.putAll(map); 159 return props; 160 } 161 162 /** Canned default for HTML output. */ 163 public static HashMap<String, String> OutputHtml() { 164 return methodMap("html"); 165 } 166 167 /** Canned default for XML output. */ 168 public static HashMap<String, String> OutputXml() { 169 return methodMap("xml"); 170 } 171 172 private static HashMap<String, String> methodMap(String method) { 173 HashMap<String, String> map = new HashMap<>(); 174 map.put(METHOD, method); 175 return map; 176 } 177 178 /** 179 * Convert a jsoup Document to a W3C Document. The created nodes will link back to the original 180 * jsoup nodes in the user property {@link #SourceProperty} (but after conversion, changes on one side will not 181 * flow to the other). 182 * 183 * @param in jsoup doc 184 * @return a W3C DOM Document representing the jsoup Document or Element contents. 185 */ 186 public Document fromJsoup(org.jsoup.nodes.Document in) { 187 // just method API backcompat 188 return fromJsoup((org.jsoup.nodes.Element) in); 189 } 190 191 /** 192 * Convert a jsoup DOM to a W3C Document. The created nodes will link back to the original 193 * jsoup nodes in the user property {@link #SourceProperty} (but after conversion, changes on one side will not 194 * flow to the other). The input Element is used as a context node, but the whole surrounding jsoup Document is 195 * converted. (If you just want a subtree converted, use {@link #convert(org.jsoup.nodes.Element, Document)}.) 196 * 197 * @param in jsoup element or doc 198 * @return a W3C DOM Document representing the jsoup Document or Element contents. 199 * @see #sourceNodes(NodeList, Class) 200 * @see #contextNode(Document) 201 */ 202 public Document fromJsoup(org.jsoup.nodes.Element in) { 203 Validate.notNull(in); 204 DocumentBuilder builder; 205 try { 206 builder = factory.newDocumentBuilder(); 207 Document out = builder.newDocument(); 208 org.jsoup.nodes.Document inDoc = in.ownerDocument(); 209 out.setXmlStandalone(true); 210 // if in is Document, use the root element, not the wrapping document, as the context: 211 org.jsoup.nodes.Element context = (in instanceof org.jsoup.nodes.Document) ? in.firstElementChild() : in; 212 out.setUserData(ContextProperty, context, null); 213 convert(inDoc != null ? inDoc : in, out); 214 return out; 215 } catch (ParserConfigurationException e) { 216 throw new IllegalStateException(e); 217 } 218 } 219 220 /** 221 * Converts a jsoup document into the provided W3C Document. If required, you can set options on the output 222 * document before converting. 223 * 224 * @param in jsoup doc 225 * @param out w3c doc 226 * @see org.jsoup.helper.W3CDom#fromJsoup(org.jsoup.nodes.Element) 227 */ 228 public void convert(org.jsoup.nodes.Document in, Document out) { 229 // just provides method API backcompat 230 convert((org.jsoup.nodes.Element) in, out); 231 } 232 233 /** 234 * Converts a jsoup element into the provided W3C Document. If required, you can set options on the output 235 * document before converting. 236 * 237 * @param in jsoup element 238 * @param out w3c doc 239 * @see org.jsoup.helper.W3CDom#fromJsoup(org.jsoup.nodes.Element) 240 */ 241 public void convert(org.jsoup.nodes.Element in, Document out) { 242 W3CBuilder builder = new W3CBuilder(out); 243 builder.namespaceAware = namespaceAware; 244 org.jsoup.nodes.Document inDoc = in.ownerDocument(); 245 if (inDoc != null) { 246 if (!StringUtil.isBlank(inDoc.location())) { 247 out.setDocumentURI(inDoc.location()); 248 } 249 builder.syntax = inDoc.outputSettings().syntax(); 250 } 251 if (in instanceof org.jsoup.nodes.Document) 252 builder.traverseDocument((org.jsoup.nodes.Document) in); 253 else 254 builder.traverse(in); 255 } 256 257 /** 258 Evaluate an XPath query against the supplied document, and return the results. 259 @param xpath an XPath query 260 @param doc the document to evaluate against 261 @return the matches nodes 262 */ 263 public NodeList selectXpath(String xpath, Document doc) { 264 return selectXpath(xpath, (Node) doc); 265 } 266 267 /** 268 Evaluate an XPath query against the supplied context node, and return the results. 269 @param xpath an XPath query 270 @param contextNode the context node to evaluate against 271 @return the matches nodes 272 */ 273 public NodeList selectXpath(String xpath, Node contextNode) { 274 Validate.notEmptyParam(xpath, "xpath"); 275 Validate.notNullParam(contextNode, "contextNode"); 276 277 NodeList nodeList; 278 try { 279 // if there is a configured XPath factory, use that instead of the Java base impl: 280 String property = System.getProperty(XPathFactoryProperty); 281 final XPathFactory xPathFactory = property != null ? 282 XPathFactory.newInstance("jsoup") : 283 XPathFactory.newInstance(); 284 285 XPathExpression expression = xPathFactory.newXPath().compile(xpath); 286 nodeList = (NodeList) expression.evaluate(contextNode, XPathConstants.NODESET); // love the strong typing here /s 287 Validate.notNull(nodeList); 288 } catch (XPathExpressionException | XPathFactoryConfigurationException e) { 289 throw new Selector.SelectorParseException( 290 e, "Could not evaluate XPath query [%s]: %s", xpath, e.getMessage()); 291 } 292 return nodeList; 293 } 294 295 /** 296 Retrieves the original jsoup DOM nodes from a nodelist created by this convertor. 297 @param nodeList the W3C nodes to get the original jsoup nodes from 298 @param nodeType the jsoup node type to retrieve (e.g. Element, DataNode, etc) 299 @param <T> node type 300 @return a list of the original nodes 301 */ 302 public <T extends org.jsoup.nodes.Node> List<T> sourceNodes(NodeList nodeList, Class<T> nodeType) { 303 Validate.notNull(nodeList); 304 Validate.notNull(nodeType); 305 List<T> nodes = new ArrayList<>(nodeList.getLength()); 306 307 for (int i = 0; i < nodeList.getLength(); i++) { 308 org.w3c.dom.Node node = nodeList.item(i); 309 Object source = node.getUserData(W3CDom.SourceProperty); 310 if (nodeType.isInstance(source)) 311 nodes.add(nodeType.cast(source)); 312 } 313 314 return nodes; 315 } 316 317 /** 318 For a Document created by {@link #fromJsoup(org.jsoup.nodes.Element)}, retrieves the W3C context node. 319 @param wDoc Document created by this class 320 @return the corresponding W3C Node to the jsoup Element that was used as the creating context. 321 */ 322 public Node contextNode(Document wDoc) { 323 return (Node) wDoc.getUserData(ContextNodeProperty); 324 } 325 326 /** 327 * Serialize a W3C document that was created by {@link #fromJsoup(org.jsoup.nodes.Element)} to a String. 328 * The output format will be XML or HTML depending on the content of the doc. 329 * 330 * @param doc Document 331 * @return Document as string 332 * @see W3CDom#asString(Document, Map) 333 */ 334 public String asString(Document doc) { 335 return asString(doc, null); 336 } 337 338 /** 339 * Implements the conversion by walking the input. 340 */ 341 protected static class W3CBuilder implements NodeVisitor { 342 private final Document doc; 343 // source bindings include omitted ancestors; output bindings track declarations emitted to the W3C tree 344 private final NamespaceBindings sourceNamespaces = new NamespaceBindings(); 345 private final NamespaceBindings outputNamespaces = new NamespaceBindings(); 346 private boolean namespaceAware = true; 347 private Node dest; 348 private Syntax syntax = Syntax.xml; // the syntax (to coerce attributes to). From the input doc if available. 349 /*@Nullable*/ private final org.jsoup.nodes.Element contextElement; // todo - unsure why this can't be marked nullable? 350 351 public W3CBuilder(Document doc) { 352 this.doc = doc; 353 dest = doc; 354 sourceNamespaces.put("xml", Parser.NamespaceXml); 355 outputNamespaces.put("xml", Parser.NamespaceXml); 356 contextElement = (org.jsoup.nodes.Element) doc.getUserData(ContextProperty); // Track the context jsoup Element, so we can save the corresponding w3c element 357 } 358 359 // Traverse only nodes supported as W3C document children, and keep the first element as the root. 360 private void traverseDocument(org.jsoup.nodes.Document source) { 361 org.jsoup.nodes.Element root = source.firstElementChild(); 362 for (org.jsoup.nodes.Node child : source.childNodes()) { 363 if (child == root || child instanceof org.jsoup.nodes.DocumentType || 364 child instanceof org.jsoup.nodes.Comment || child instanceof org.jsoup.nodes.XmlDeclaration) 365 traverse(child); 366 } 367 } 368 369 @Override 370 public void head(org.jsoup.nodes.Node source, int depth) { 371 if (source instanceof org.jsoup.nodes.Element) 372 appendElement((org.jsoup.nodes.Element) source, depth); 373 else if (source instanceof org.jsoup.nodes.DocumentType) 374 appendDocumentType((org.jsoup.nodes.DocumentType) source); 375 else if (source instanceof CDataNode) 376 appendCdata((CDataNode) source); 377 else if (source instanceof TextNode) 378 append(doc.createTextNode(((TextNode) source).getWholeText()), source); 379 else if (source instanceof Comment) 380 append(doc.createComment(((Comment) source).getData()), source); 381 else if (source instanceof DataNode) 382 append(doc.createTextNode(((DataNode) source).getWholeData()), source); 383 else if (source instanceof XmlDeclaration) 384 appendProcessingInstruction((XmlDeclaration) source); 385 386 } 387 388 /** Converts and appends an element, descending into its output node when representable. */ 389 private void appendElement(org.jsoup.nodes.Element source, int depth) { 390 if (depth == 0) 391 seedSourceNamespaces(source); 392 sourceNamespaces.pushScope(); 393 outputNamespaces.pushScope(); 394 sourceNamespaces.applyDeclarations(source.attributes()); 395 String namespace = namespaceAware ? w3cNamespace(source) : null; 396 String tagName = w3cSafeName(source.tagName(), Syntax.xml); 397 Element el; 398 try { 399 // use an empty namespace if none is present but the tag name has a prefix 400 String imputedNamespace = namespace == null && tagName.contains(":") ? "" : namespace; 401 el = doc.createElementNS(imputedNamespace, tagName); 402 } catch (DOMException ignored) { 403 // If the Normalize didn't get it XML / W3C safe, inserts as plain text 404 append(doc.createTextNode("<" + tagName + ">"), source); 405 return; 406 } 407 copyAttributes(source, el); 408 append(el, source); 409 if (source == contextElement) 410 doc.setUserData(ContextNodeProperty, el, null); 411 dest = el; // descend 412 } 413 414 // Keep the doctype in document order; invalid doctypes cannot be represented. 415 private void appendDocumentType(org.jsoup.nodes.DocumentType source) { 416 try { 417 DocumentType type = doc.getImplementation().createDocumentType(source.name(), source.publicId(), source.systemId()); 418 append(type, source); 419 } catch (DOMException ignored) { 420 // invalid / empty doctype dropped 421 } 422 } 423 424 // Preserve CDATA where possible; programmatic content may be invalid for a W3C CDATA node. 425 private void appendCdata(org.jsoup.nodes.CDataNode source) { 426 try { 427 append(doc.createCDATASection(source.getWholeText()), source); 428 } catch (DOMException ignored) { 429 append(doc.createTextNode(source.getWholeText()), source); 430 } 431 } 432 433 // XmlDeclaration also represents <!name ...> nodes; XML declarations are reserved by the W3C DOM. 434 private void appendProcessingInstruction(org.jsoup.nodes.XmlDeclaration source) { 435 if (!source.outerHtml().startsWith("<?") || source.name().equalsIgnoreCase("xml")) return; 436 try { 437 append(doc.createProcessingInstruction(source.name(), source.getWholeDeclaration()), source); 438 } catch (DOMException ignored) { 439 // invalid programmatic processing instruction dropped 440 } 441 } 442 443 private static @Nullable String w3cNamespace(org.jsoup.nodes.Element sourceEl) { 444 // In W3C DOM, plain XML elements have no namespace; XML namespace is reserved for the {@code xml} prefix 445 String namespace = sourceEl.tag().namespace(); 446 if (Parser.NamespaceXml.equals(namespace) && sourceEl.tag().prefix().isEmpty()) 447 return null; 448 return namespace; 449 } 450 451 /** Applies declarations inherited from ancestors outside a subtree conversion. */ 452 private void seedSourceNamespaces(org.jsoup.nodes.Element sourceEl) { 453 org.jsoup.select.Elements parents = sourceEl.parents(); 454 for (int i = parents.size() - 1; i >= 0; i--) { 455 org.jsoup.nodes.Element parent = parents.get(i); 456 if (parent.attributesSize() > 0) 457 sourceNamespaces.applyDeclarations(parent.attributes()); 458 } 459 } 460 461 private void append(Node append, org.jsoup.nodes.Node source) { 462 append.setUserData(SourceProperty, source, null); 463 dest.appendChild(append); 464 } 465 466 @Override 467 public void tail(org.jsoup.nodes.Node source, int depth) { 468 // head may emit an unrepresentable element as text without descending, so only ascend from its matching output element 469 if (source instanceof org.jsoup.nodes.Element && dest.getUserData(SourceProperty) == source && 470 dest.getParentNode() != null) { 471 dest = dest.getParentNode(); // undescend 472 } 473 if (source instanceof org.jsoup.nodes.Element) { 474 sourceNamespaces.popScope(); 475 outputNamespaces.popScope(); 476 } 477 } 478 479 /** Copies namespace declarations first so source attribute order does not affect binding resolution. */ 480 private void copyAttributes(org.jsoup.nodes.Element jEl, Element wEl) { 481 Attributes attributes = jEl.attributes(); 482 for (Attribute attribute : attributes) { 483 if (NamespaceBindings.isDeclaration(attribute.getKey())) 484 copyAttribute(wEl, attribute); 485 } 486 for (Attribute attribute : attributes) { 487 if (!NamespaceBindings.isDeclaration(attribute.getKey())) 488 copyAttribute(wEl, attribute); 489 } 490 } 491 492 /** Copies an attribute using the closest W3C representation. */ 493 private void copyAttribute(Element wEl, Attribute attribute) { 494 // preserve DOM-compatible HTML names; otherwise normalize as XML, and skip if still unrepresentable 495 if (!trySetAttribute(wEl, attribute, syntax) && syntax != Syntax.xml) 496 trySetAttribute(wEl, attribute, Syntax.xml); 497 } 498 499 /** Tries to copy an attribute, allowing the DOM to validate its name and namespace. */ 500 private boolean trySetAttribute(Element wEl, Attribute attribute, Syntax syntax) { 501 try { 502 setAttribute(wEl, attribute, syntax); 503 return true; 504 } catch (DOMException ignored) { 505 return false; 506 } 507 } 508 509 /** Copies an attribute with its resolved namespace and W3C-safe name. */ 510 private void setAttribute(Element wEl, Attribute attribute, Syntax syntax) throws DOMException { 511 String key = w3cSafeName(attribute.getKey(), syntax); 512 513 @Nullable String declarationPrefix = NamespaceBindings.declarationPrefix(key); 514 if (declarationPrefix != null) { 515 setNamespaceDeclaration(wEl, key, attribute.getValue()); 516 outputNamespaces.put(declarationPrefix, attribute.getValue()); 517 return; 518 } 519 520 int pos = key.indexOf(':'); 521 if (pos == -1) { // default namespaces do not apply to unprefixed attributes 522 wEl.setAttribute(key, attribute.getValue()); 523 return; 524 } 525 526 String prefix = key.substring(0, pos); 527 String sourcePrefix = attribute.prefix(); 528 @Nullable String namespace; 529 if (namespaceAware) { 530 String attributeNamespace = attribute.namespace(); 531 namespace = !attributeNamespace.isEmpty() ? attributeNamespace : sourceNamespaces.get(sourcePrefix); 532 } else { 533 namespace = sourceNamespaces.get(sourcePrefix); 534 } 535 if (namespace == null || namespace.isEmpty()) 536 namespace = undefinedNs; 537 538 if (namespaceAware) 539 wEl.setAttributeNS(namespace, key, attribute.getValue()); 540 else 541 wEl.setAttribute(key, attribute.getValue()); 542 ensureOutputBinding(wEl, prefix, namespace); 543 } 544 545 /** Normalizes a name to a W3C-compatible QName, converting {@code 1:a:b} to {@code _1:a_b}. */ 546 private String w3cSafeName(String name, Syntax syntax) { 547 String normalized = Attribute.getValidKey(name, syntax); 548 if (normalized.indexOf(':') == -1) return normalized; 549 550 Matcher parts = QNameParts.matcher(normalized); 551 if (!parts.matches()) return w3cSafeNcName(normalized.replace(':', '_')); 552 return w3cSafeNcName(parts.group(1)) + ':' + w3cSafeNcName(parts.group(2)); 553 } 554 555 /** Normalizes one QName component while preserving valid XML name characters. */ 556 private String w3cSafeNcName(String name) { 557 if (isValidNcName(name)) return name; 558 String safeStart = StringUtil.concat('_', name); 559 if (isValidNcName(safeStart)) return safeStart; 560 561 String colonSafe = name.replace(':', '_'); 562 return Attribute.getValidKey(colonSafe, Syntax.xml); 563 } 564 565 /** Tests a component against the XML NCName rules used by the output DOM. */ 566 private boolean isValidNcName(String name) { 567 try { 568 // validate as a local name so reserved words such as xmlns are treated as ordinary NCName text 569 doc.createAttributeNS(undefinedNs, "p:" + name); 570 return true; 571 } catch (DOMException ignored) { 572 return false; 573 } 574 } 575 576 /** Declares a prefix when its output binding is not active. */ 577 private void ensureOutputBinding(Element wEl, String prefix, String namespace) { 578 if (!namespace.equals(outputNamespaces.get(prefix))) { 579 setNamespaceDeclaration(wEl, "xmlns:" + prefix, namespace); 580 outputNamespaces.put(prefix, namespace); 581 } 582 } 583 584 /** Writes a namespace declaration with namespace awareness when enabled. */ 585 private void setNamespaceDeclaration(Element wEl, String key, String namespace) { 586 if (namespaceAware) 587 wEl.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, key, namespace); 588 else 589 wEl.setAttribute(key, namespace); 590 } 591 592 private static final Pattern QNameParts = Pattern.compile("^([^:]+):(.+)$"); 593 private static final String undefinedNs = "undefined"; 594 } 595 596}