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}