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}