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