1 /*
2 * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.opensaml.xml.util;
18
19 import java.io.OutputStream;
20 import java.io.StringWriter;
21 import java.io.Writer;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.GregorianCalendar;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Map;
29 import java.util.StringTokenizer;
30 import java.util.Map.Entry;
31
32 import javax.xml.datatype.DatatypeConfigurationException;
33 import javax.xml.datatype.DatatypeFactory;
34 import javax.xml.datatype.Duration;
35 import javax.xml.namespace.QName;
36
37 import org.opensaml.xml.Configuration;
38 import org.opensaml.xml.XMLObject;
39 import org.opensaml.xml.XMLRuntimeException;
40 import org.opensaml.xml.parse.XMLParserException;
41 import org.w3c.dom.Attr;
42 import org.w3c.dom.DOMConfiguration;
43 import org.w3c.dom.DOMImplementation;
44 import org.w3c.dom.Document;
45 import org.w3c.dom.Element;
46 import org.w3c.dom.NamedNodeMap;
47 import org.w3c.dom.Node;
48 import org.w3c.dom.NodeList;
49 import org.w3c.dom.Text;
50 import org.w3c.dom.ls.DOMImplementationLS;
51 import org.w3c.dom.ls.LSOutput;
52 import org.w3c.dom.ls.LSSerializer;
53 import org.w3c.dom.ls.LSSerializerFilter;
54
55 /**
56 * A helper class for working with W3C DOM objects.
57 */
58 public final class XMLHelper {
59
60 /**
61 * A string which contains the valid delimiters for the XML Schema 'list' type. These are: space, newline, carriage
62 * return, and tab.
63 */
64 public static final String LIST_DELIMITERS = " \n\r\t";
65
66 /** DOM configuration parameters used by LSSerializer in pretty print format output. */
67 private static Map<String, Object> prettyPrintParams;
68
69 /** JAXP DatatypeFactory. */
70 private static DatatypeFactory dataTypeFactory;
71
72 /** Constructor. */
73 private XMLHelper() {
74
75 }
76
77 /**
78 * Gets a static instance of a JAXP DatatypeFactory.
79 *
80 * @return the factory or null if the factory could not be created
81 */
82 public static DatatypeFactory getDataTypeFactory() {
83 if (dataTypeFactory == null) {
84 try {
85 dataTypeFactory = DatatypeFactory.newInstance();
86 } catch (DatatypeConfigurationException e) {
87 // do nothing
88 }
89 }
90
91 return dataTypeFactory;
92 }
93
94 /**
95 * Checks if the given element has an xsi:type defined for it.
96 *
97 * @param e the DOM element
98 *
99 * @return true if there is a type, false if not
100 */
101 public static boolean hasXSIType(Element e) {
102 if (e != null) {
103 if (e.getAttributeNodeNS(XMLConstants.XSI_NS, "type") != null) {
104 return true;
105 }
106 }
107
108 return false;
109 }
110
111 /**
112 * Gets the XSI type for a given element if it has one.
113 *
114 * @param e the element
115 *
116 * @return the type or null
117 */
118 public static QName getXSIType(Element e) {
119 if (hasXSIType(e)) {
120 Attr attribute = e.getAttributeNodeNS(XMLConstants.XSI_NS, "type");
121 String attributeValue = attribute.getTextContent().trim();
122 StringTokenizer tokenizer = new StringTokenizer(attributeValue, ":");
123 String prefix = null;
124 String localPart;
125 if (tokenizer.countTokens() > 1) {
126 prefix = tokenizer.nextToken();
127 localPart = tokenizer.nextToken();
128 } else {
129 localPart = tokenizer.nextToken();
130 }
131
132 return constructQName(e.lookupNamespaceURI(prefix), localPart, prefix);
133 }
134
135 return null;
136 }
137
138 /**
139 * Gets the ID attribute of a DOM element.
140 *
141 * @param domElement the DOM element
142 *
143 * @return the ID attribute or null if there isn't one
144 */
145 public static Attr getIdAttribute(Element domElement) {
146 if (!domElement.hasAttributes()) {
147 return null;
148 }
149
150 NamedNodeMap attributes = domElement.getAttributes();
151 Attr attribute;
152 for (int i = 0; i < attributes.getLength(); i++) {
153 attribute = (Attr) attributes.item(i);
154 if (attribute.isId()) {
155 return attribute;
156 }
157 }
158
159 return null;
160 }
161
162 /**
163 * Gets the QName for the given DOM node.
164 *
165 * @param domNode the DOM node
166 *
167 * @return the QName for the element or null if the element was null
168 */
169 public static QName getNodeQName(Node domNode) {
170 if (domNode != null) {
171 return constructQName(domNode.getNamespaceURI(), domNode.getLocalName(), domNode.getPrefix());
172 }
173
174 return null;
175 }
176
177 /**
178 * Gets the lcoale currently active for the element. This is done by looking for an xml:lang attribute and parsing
179 * its content. If no xml:lang attribute is present the default locale is returned. This method only uses the
180 * language primary tag, as defined by RFC3066.
181 *
182 * @param element element to retrieve local information for
183 *
184 * @return the active local of the element
185 */
186 public static Locale getLanguage(Element element) {
187 String lang = DatatypeHelper.safeTrimOrNullString(element.getAttributeNS(XMLConstants.XML_NS, "lang"));
188 if (lang != null) {
189 if (lang.contains("-")) {
190 lang = lang.substring(0, lang.indexOf("-"));
191 }
192 return new Locale(lang.toUpperCase());
193 } else {
194 return Locale.getDefault();
195 }
196 }
197
198 /**
199 * Constructs an attribute owned by the given document with the given name.
200 *
201 * @param owningDocument the owning document
202 * @param attributeName the name of that attribute
203 *
204 * @return the constructed attribute
205 */
206 public static Attr constructAttribute(Document owningDocument, QName attributeName) {
207 return constructAttribute(owningDocument, attributeName.getNamespaceURI(), attributeName.getLocalPart(),
208 attributeName.getPrefix());
209 }
210
211 /**
212 * Constructs an attribute owned by the given document with the given name.
213 *
214 * @param document the owning document
215 * @param namespaceURI the URI for the namespace the attribute is in
216 * @param localName the local name
217 * @param prefix the prefix of the namespace that attribute is in
218 *
219 * @return the constructed attribute
220 */
221 public static Attr constructAttribute(Document document, String namespaceURI, String localName, String prefix) {
222 String trimmedLocalName = DatatypeHelper.safeTrimOrNullString(localName);
223
224 if (trimmedLocalName == null) {
225 throw new IllegalArgumentException("Local name may not be null or empty");
226 }
227
228 String qualifiedName;
229 String trimmedPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
230 if (trimmedPrefix != null) {
231 qualifiedName = trimmedPrefix + ":" + DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
232 } else {
233 qualifiedName = DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
234 }
235
236 if (DatatypeHelper.isEmpty(namespaceURI)) {
237 return document.createAttributeNS(null, qualifiedName);
238 } else {
239 return document.createAttributeNS(namespaceURI, qualifiedName);
240 }
241 }
242
243 /**
244 * Constructs a QName from an attributes value.
245 *
246 * @param attribute the attribute with a QName value
247 *
248 * @return a QName from an attributes value, or null if the given attribute is null
249 */
250 public static QName getAttributeValueAsQName(Attr attribute) {
251 if (attribute == null || DatatypeHelper.isEmpty(attribute.getValue())) {
252 return null;
253 }
254
255 String attributeValue = attribute.getTextContent();
256 String[] valueComponents = attributeValue.split(":");
257 if (valueComponents.length == 1) {
258 return constructQName(attribute.lookupNamespaceURI(null), valueComponents[0], null);
259 } else {
260 return constructQName(attribute.lookupNamespaceURI(valueComponents[0]), valueComponents[1],
261 valueComponents[0]);
262 }
263 }
264
265 /**
266 * Parses the attribute's value. If the value is 0 or "false" then false is returned, if the value is 1 or "true"
267 * then true is returned, if the value is anything else then null returned.
268 *
269 * @param attribute attribute whose value will be converted to a boolean
270 *
271 * @return boolean value of the attribute or null
272 */
273 public static Boolean getAttributeValueAsBoolean(Attr attribute) {
274 if (attribute == null) {
275 return null;
276 }
277
278 String valueStr = attribute.getValue();
279 if (valueStr.equals("0") || valueStr.equals("false")) {
280 return Boolean.FALSE;
281 } else if (valueStr.equals("1") || valueStr.equals("true")) {
282 return Boolean.TRUE;
283 } else {
284 return null;
285 }
286 }
287
288 /**
289 * Gets the value of a list-type attribute as a list.
290 *
291 * @param attribute attribute whose value will be turned into a list
292 *
293 * @return list of values, never null
294 */
295 public static List<String> getAttributeValueAsList(Attr attribute) {
296 if (attribute == null) {
297 return Collections.emptyList();
298 }
299 return DatatypeHelper.stringToList(attribute.getValue(), LIST_DELIMITERS);
300 }
301
302 /**
303 * Marshall an attribute name and value to a DOM Element. This is particularly useful for attributes whose names
304 * appear in namespace-qualified form.
305 *
306 * @param attributeName the attribute name in QName form
307 * @param attributeValue the attribute value
308 * @param domElement the target element to which to marshall
309 * @param isIDAttribute flag indicating whether the attribute being marshalled should be handled as an ID-typed
310 * attribute
311 */
312 public static void marshallAttribute(QName attributeName, String attributeValue, Element domElement,
313 boolean isIDAttribute) {
314 Document document = domElement.getOwnerDocument();
315 Attr attribute = XMLHelper.constructAttribute(document, attributeName);
316 attribute.setValue(attributeValue);
317 domElement.setAttributeNodeNS(attribute);
318 if (isIDAttribute) {
319 domElement.setIdAttributeNode(attribute, true);
320 }
321 }
322
323 /**
324 * Marshall an attribute name and value to a DOM Element. This is particularly useful for attributes whose names
325 * appear in namespace-qualified form.
326 *
327 * @param attributeName the attribute name in QName form
328 * @param attributeValues the attribute values
329 * @param domElement the target element to which to marshall
330 * @param isIDAttribute flag indicating whether the attribute being marshalled should be handled as an ID-typed
331 * attribute
332 */
333 public static void marshallAttribute(QName attributeName, List<String> attributeValues, Element domElement,
334 boolean isIDAttribute) {
335 marshallAttribute(attributeName, DatatypeHelper.listToStringValue(attributeValues, " "), domElement,
336 isIDAttribute);
337 }
338
339 /**
340 * Marshall the attributes represented by the indicated AttributeMap into the indicated DOM Element.
341 *
342 * @param attributeMap the AttributeMap
343 * @param domElement the target Element
344 */
345 public static void marshallAttributeMap(AttributeMap attributeMap, Element domElement) {
346 Document document = domElement.getOwnerDocument();
347 Attr attribute = null;
348 for (Entry<QName, String> entry : attributeMap.entrySet()) {
349 attribute = XMLHelper.constructAttribute(document, entry.getKey());
350 attribute.setValue(entry.getValue());
351 domElement.setAttributeNodeNS(attribute);
352 if (Configuration.isIDAttribute(entry.getKey()) || attributeMap.isIDAttribute(entry.getKey())) {
353 domElement.setIdAttributeNode(attribute, true);
354 }
355 }
356 }
357
358 /**
359 * Unmarshall a DOM Attr to an AttributeMap.
360 *
361 * @param attributeMap the target AttributeMap
362 * @param attribute the target DOM Attr
363 */
364 public static void unmarshallToAttributeMap(AttributeMap attributeMap, Attr attribute) {
365 QName attribQName = XMLHelper.constructQName(attribute.getNamespaceURI(), attribute.getLocalName(), attribute
366 .getPrefix());
367 attributeMap.put(attribQName, attribute.getValue());
368 if (attribute.isId() || Configuration.isIDAttribute(attribQName)) {
369 attributeMap.registerID(attribQName);
370 }
371 }
372
373 /**
374 * Constructs a QName from an element's adjacent Text child nodes.
375 *
376 * @param element the element with a QName value
377 *
378 * @return a QName from an element's value, or null if the given element is empty
379 */
380 public static QName getElementContentAsQName(Element element) {
381 if (element == null) {
382 return null;
383 }
384
385 String elementContent = null;
386 NodeList nodeList = element.getChildNodes();
387 for (int i = 0; i < nodeList.getLength(); i++) {
388 Node node = nodeList.item(i);
389 if (node.getNodeType() == Node.TEXT_NODE) {
390 elementContent = DatatypeHelper.safeTrimOrNullString(((Text) node).getWholeText());
391 break;
392 }
393 }
394
395 if (elementContent == null) {
396 return null;
397 }
398
399 String[] valueComponents = elementContent.split(":");
400 if (valueComponents.length == 1) {
401 return constructQName(element.lookupNamespaceURI(null), valueComponents[0], null);
402 } else {
403 return constructQName(element.lookupNamespaceURI(valueComponents[0]), valueComponents[1],
404 valueComponents[0]);
405 }
406 }
407
408 /**
409 * Gets the value of a list-type element as a list.
410 *
411 * @param element element whose value will be turned into a list
412 *
413 * @return list of values, never null
414 */
415 public static List<String> getElementContentAsList(Element element) {
416 if (element == null) {
417 return Collections.emptyList();
418 }
419 return DatatypeHelper.stringToList(element.getTextContent(), LIST_DELIMITERS);
420 }
421
422 /**
423 * Constructs a QName.
424 *
425 * @param namespaceURI the namespace of the QName
426 * @param localName the local name of the QName
427 * @param prefix the prefix of the QName, may be null
428 *
429 * @return the QName
430 */
431 public static QName constructQName(String namespaceURI, String localName, String prefix) {
432 if (DatatypeHelper.isEmpty(prefix)) {
433 return new QName(namespaceURI, localName);
434 } else if (DatatypeHelper.isEmpty(namespaceURI)) {
435 return new QName(localName);
436 }
437
438 return new QName(namespaceURI, localName, prefix);
439 }
440
441 /**
442 * Constructs a QName from a string (attribute or element content) value.
443 *
444 * @param qname the QName string
445 * @param owningObject XMLObject, with cached DOM, owning the QName
446 *
447 * @return the QName respresented by the string
448 */
449 public static QName constructQName(String qname, XMLObject owningObject) {
450 return constructQName(qname, owningObject.getDOM());
451 }
452
453 /**
454 * Constructs a QName from a string (attribute element content) value.
455 *
456 * @param qname the QName string
457 * @param owningElement parent DOM element of the Node which contains the QName value
458 *
459 * @return the QName respresented by the string
460 */
461 public static QName constructQName(String qname, Element owningElement) {
462 String nsURI;
463 String nsPrefix;
464 String name;
465
466 if (qname.indexOf(":") > -1) {
467 StringTokenizer qnameTokens = new StringTokenizer(qname, ":");
468 nsPrefix = qnameTokens.nextToken();
469 name = qnameTokens.nextToken();
470 } else {
471 nsPrefix = "";
472 name = qname;
473 }
474
475 nsURI = lookupNamespaceURI(owningElement, nsPrefix);
476 return constructQName(nsURI, name, nsPrefix);
477 }
478
479 /**
480 * Constructs an element, rooted in the given document, with the given name.
481 *
482 * @param document the document containing the element
483 * @param elementName the name of the element, must contain a local name, may contain a namespace URI and prefix
484 *
485 * @return the element
486 */
487 public static Element constructElement(Document document, QName elementName) {
488 return constructElement(document, elementName.getNamespaceURI(), elementName.getLocalPart(), elementName
489 .getPrefix());
490 }
491
492 /**
493 * Constructs an element, rooted in the given document, with the given information.
494 *
495 * @param document the document containing the element
496 * @param namespaceURI the URI of the namespace the element is in
497 * @param localName the element's local name
498 * @param prefix the prefix of the namespace the element is in
499 *
500 * @return the element
501 */
502 public static Element constructElement(Document document, String namespaceURI, String localName, String prefix) {
503 String trimmedLocalName = DatatypeHelper.safeTrimOrNullString(localName);
504
505 if (trimmedLocalName == null) {
506 throw new IllegalArgumentException("Local name may not be null or empty");
507 }
508
509 String qualifiedName;
510 String trimmedPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
511 if (trimmedPrefix != null) {
512 qualifiedName = trimmedPrefix + ":" + DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
513 } else {
514 qualifiedName = DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
515 }
516
517 if (!DatatypeHelper.isEmpty(namespaceURI)) {
518 return document.createElementNS(namespaceURI, qualifiedName);
519 } else {
520 return document.createElementNS(null, qualifiedName);
521 }
522 }
523
524 /**
525 * Appends the child Element to the parent Element, adopting the child Element into the parent's Document if needed.
526 *
527 * @param parentElement the parent Element
528 * @param childElement the child Element
529 */
530 public static void appendChildElement(Element parentElement, Element childElement) {
531 Document parentDocument = parentElement.getOwnerDocument();
532 adoptElement(childElement, parentDocument);
533
534 parentElement.appendChild(childElement);
535 }
536
537 /**
538 * Adopts an element into a document if the child is not already in the document.
539 *
540 * @param adoptee the element to be adopted
541 * @param adopter the document into which the element is adopted
542 */
543 public static void adoptElement(Element adoptee, Document adopter) {
544 if (!(adoptee.getOwnerDocument().equals(adopter))) {
545 if (adopter.adoptNode(adoptee) == null) {
546 // This can happen if the adopter and adoptee were produced by different DOM implementations
547 throw new XMLRuntimeException("DOM Element node adoption failed");
548 }
549 }
550 }
551
552 /**
553 * Creates a text node with the given content and appends it as child to the given element.
554 *
555 * @param domElement the element to recieve the text node
556 * @param textContent the content for the text node
557 */
558 public static void appendTextContent(Element domElement, String textContent) {
559 if (textContent == null) {
560 return;
561 }
562 Document parentDocument = domElement.getOwnerDocument();
563 Text textNode = parentDocument.createTextNode(textContent);
564 domElement.appendChild(textNode);
565 }
566
567 /**
568 * Adds a namespace declaration (xmlns:) attribute to the given element.
569 *
570 * @param domElement the element to add the attribute to
571 * @param namespaceURI the URI of the namespace
572 * @param prefix the prefix for the namespace
573 */
574 public static void appendNamespaceDeclaration(Element domElement, String namespaceURI, String prefix) {
575 String nsURI = DatatypeHelper.safeTrimOrNullString(namespaceURI);
576 String nsPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
577
578 String attributeName;
579 if (nsPrefix == null) {
580 attributeName = XMLConstants.XMLNS_PREFIX;
581 } else {
582 attributeName = XMLConstants.XMLNS_PREFIX + ":" + nsPrefix;
583 }
584
585 String attributeValue;
586 if (nsURI == null) {
587 attributeValue = "";
588 } else {
589 attributeValue = nsURI;
590 }
591
592 domElement.setAttributeNS(XMLConstants.XMLNS_NS, attributeName, attributeValue);
593 }
594
595 /**
596 * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
597 * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
598 * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
599 * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
600 * doesn't have an namespace delcaration attribute.
601 *
602 * @param startingElement the starting element
603 * @param prefix the prefix to look up
604 *
605 * @return the namespace URI for the given prefix
606 */
607 public static String lookupNamespaceURI(Element startingElement, String prefix) {
608 return lookupNamespaceURI(startingElement, null, prefix);
609 }
610
611 /**
612 * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
613 * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
614 * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
615 * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
616 * doesn't have an namespace delcaration attribute.
617 *
618 * @param startingElement the starting element
619 * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the
620 * search
621 * @param prefix the prefix to look up
622 *
623 * @return the namespace URI for the given prefer or null
624 */
625 public static String lookupNamespaceURI(Element startingElement, Element stopingElement, String prefix) {
626 String namespaceURI;
627
628 // This code is a modified version of the lookup code within Xerces
629 if (startingElement.hasAttributes()) {
630 NamedNodeMap map = startingElement.getAttributes();
631 int length = map.getLength();
632 for (int i = 0; i < length; i++) {
633 Node attr = map.item(i);
634 String attrPrefix = attr.getPrefix();
635 String value = attr.getNodeValue();
636 namespaceURI = attr.getNamespaceURI();
637 if (namespaceURI != null && namespaceURI.equals(XMLConstants.XMLNS_NS)) {
638 // at this point we are dealing with DOM Level 2 nodes only
639 if (prefix == null && attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)) {
640 // default namespace
641 return value;
642 } else if (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX)
643 && attr.getLocalName().equals(prefix)) {
644 // non default namespace
645 return value;
646 }
647 }
648 }
649 }
650
651 if (startingElement != stopingElement) {
652 Element ancestor = getElementAncestor(startingElement);
653 if (ancestor != null) {
654 return lookupNamespaceURI(ancestor, stopingElement, prefix);
655 }
656 }
657
658 return null;
659 }
660
661 /**
662 * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
663 * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
664 * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
665 * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
666 * doesn't have an namespace delcaration attribute.
667 *
668 * @param startingElement the starting element
669 * @param namespaceURI the uri to look up
670 *
671 * @return the prefix for the given namespace URI
672 */
673 public static String lookupPrefix(Element startingElement, String namespaceURI) {
674 return lookupPrefix(startingElement, null, namespaceURI);
675 }
676
677 /**
678 * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
679 * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
680 * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
681 * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
682 * doesn't have an namespace delcaration attribute.
683 *
684 * @param startingElement the starting element
685 * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the
686 * search
687 * @param namespaceURI the uri to look up
688 *
689 * @return the prefix for the given namespace URI
690 */
691 public static String lookupPrefix(Element startingElement, Element stopingElement, String namespaceURI) {
692 String namespace;
693
694 // This code is a modified version of the lookup code within Xerces
695 if (startingElement.hasAttributes()) {
696 NamedNodeMap map = startingElement.getAttributes();
697 int length = map.getLength();
698 for (int i = 0; i < length; i++) {
699 Node attr = map.item(i);
700 String attrPrefix = attr.getPrefix();
701 String value = attr.getNodeValue();
702 namespace = attr.getNamespaceURI();
703 if (namespace != null && namespace.equals(XMLConstants.XMLNS_NS)) {
704 // DOM Level 2 nodes
705 if (attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)
706 || (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX))
707 && value.equals(namespaceURI)) {
708
709 String localname = attr.getLocalName();
710 String foundNamespace = startingElement.lookupNamespaceURI(localname);
711 if (foundNamespace != null && foundNamespace.equals(namespaceURI)) {
712 return localname;
713 }
714 }
715
716 }
717 }
718 }
719
720 if (startingElement != stopingElement) {
721 Element ancestor = getElementAncestor(startingElement);
722 if (ancestor != null) {
723 return lookupPrefix(ancestor, stopingElement, namespaceURI);
724 }
725 }
726
727 return null;
728 }
729
730 /**
731 * Gets the child nodes with the given namespace qualified tag name. If you need to retrieve multiple, named,
732 * children consider using {@link #getChildElements(Element)}.
733 *
734 * @param root element to retrieve the children from
735 * @param namespaceURI namespace URI of the child element
736 * @param localName local, tag, name of the child element
737 *
738 * @return list of child elements, never null
739 */
740 public static List<Element> getChildElementsByTagNameNS(Element root, String namespaceURI, String localName) {
741 ArrayList<Element> children = new ArrayList<Element>();
742 NodeList childNodes = root.getChildNodes();
743
744 int numOfNodes = childNodes.getLength();
745 Node childNode;
746 Element e;
747 for (int i = 0; i < numOfNodes; i++) {
748 childNode = childNodes.item(i);
749 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
750 e = (Element) childNode;
751 if (DatatypeHelper.safeEquals(e.getNamespaceURI(), namespaceURI)
752 && DatatypeHelper.safeEquals(e.getLocalName(), localName)) {
753 children.add(e);
754 }
755 }
756 }
757
758 return children;
759 }
760
761 /**
762 * Gets the child nodes with the given local tag name. If you need to retrieve multiple, named, children consider
763 * using {@link #getChildElements(Element)}.
764 *
765 * @param root element to retrieve the children from
766 * @param localName local, tag, name of the child element
767 *
768 * @return list of child elements, never null
769 */
770 public static List<Element> getChildElementsByTagName(Element root, String localName) {
771 ArrayList<Element> children = new ArrayList<Element>();
772 NodeList childNodes = root.getChildNodes();
773
774 int numOfNodes = childNodes.getLength();
775 Node childNode;
776 Element e;
777 for (int i = 0; i < numOfNodes; i++) {
778 childNode = childNodes.item(i);
779 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
780 e = (Element) childNode;
781 if (DatatypeHelper.safeEquals(e.getLocalName(), localName)) {
782 children.add(e);
783 }
784 }
785 }
786
787 return children;
788 }
789
790 /**
791 * Gets the child elements of the given element in a single iteration.
792 *
793 * @param root element to get the child elements of
794 *
795 * @return child elements indexed by namespace qualifed tag name, never null
796 */
797 public static Map<QName, List<Element>> getChildElements(Element root) {
798 Map<QName, List<Element>> children = new HashMap<QName, List<Element>>();
799 NodeList childNodes = root.getChildNodes();
800
801 int numOfNodes = childNodes.getLength();
802 Node childNode;
803 Element e;
804 QName qname;
805 List<Element> elements;
806 for (int i = 0; i < numOfNodes; i++) {
807 childNode = childNodes.item(i);
808 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
809 e = (Element) childNode;
810 qname = getNodeQName(e);
811 elements = children.get(qname);
812 if (elements == null) {
813 elements = new ArrayList<Element>();
814 children.put(qname, elements);
815 }
816
817 elements.add(e);
818 }
819 }
820
821 return children;
822 }
823
824 /**
825 * Gets the ancestor element node to the given node.
826 *
827 * @param currentNode the node to retrive the ancestor for
828 *
829 * @return the ancestral element node of the current node, or null
830 */
831 public static Element getElementAncestor(Node currentNode) {
832 Node parent = currentNode.getParentNode();
833 if (parent != null) {
834 short type = parent.getNodeType();
835 if (type == Node.ELEMENT_NODE) {
836 return (Element) parent;
837 }
838 return getElementAncestor(parent);
839 }
840 return null;
841 }
842
843 /**
844 * Converts a Node into a String using the DOM, level 3, Load/Save serializer.
845 *
846 * @param node the node to be written to a string
847 *
848 * @return the string representation of the node
849 */
850 public static String nodeToString(Node node) {
851 StringWriter writer = new StringWriter();
852 writeNode(node, writer);
853 return writer.toString();
854 }
855
856 /**
857 * Pretty prints the XML node.
858 *
859 * @param node xml node to print
860 *
861 * @return pretty-printed xml
862 */
863 public static String prettyPrintXML(Node node) {
864 StringWriter writer = new StringWriter();
865 writeNode(node, writer, getPrettyPrintParams());
866 return writer.toString();
867 }
868
869 /**
870 * Create the parameters set used in pretty print formatting of an LSSerializer.
871 *
872 * @return the params map
873 */
874 private static Map<String, Object> getPrettyPrintParams() {
875 if (prettyPrintParams == null) {
876 prettyPrintParams = new LazyMap<String, Object>();
877 prettyPrintParams.put("format-pretty-print", Boolean.TRUE);
878 }
879 return prettyPrintParams;
880 }
881
882 /**
883 * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The written content is encoded using
884 * the encoding specified in the writer configuration.
885 *
886 * @param node the node to write out
887 * @param output the writer to write the XML to
888 */
889 public static void writeNode(Node node, Writer output) {
890 writeNode(node, output, null);
891 }
892
893 /**
894 * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The written content is encoded using
895 * the encoding specified in the writer configuration.
896 *
897 * @param node the node to write out
898 * @param output the writer to write the XML to
899 * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
900 * instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
901 */
902 public static void writeNode(Node node, Writer output, Map<String, Object> serializerParams) {
903 DOMImplementationLS domImplLS = getLSDOMImpl(node);
904
905 LSSerializer serializer = getLSSerializer(domImplLS, serializerParams);
906
907 LSOutput serializerOut = domImplLS.createLSOutput();
908 serializerOut.setCharacterStream(output);
909
910 serializer.write(node, serializerOut);
911 }
912
913 /**
914 * Writes a Node out to an OutputStream using the DOM, level 3, Load/Save serializer. The written content
915 * is encoded using the encoding specified in the output stream configuration.
916 *
917 * @param node the node to write out
918 * @param output the output stream to write the XML to
919 */
920 public static void writeNode(Node node, OutputStream output) {
921 writeNode(node, output, null);
922 }
923
924
925 /**
926 * Writes a Node out to an OutputStream using the DOM, level 3, Load/Save serializer. The written content
927 * is encoded using the encoding specified in the output stream configuration.
928 *
929 * @param node the node to write out
930 * @param output the output stream to write the XML to
931 * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
932 * instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
933 */
934 public static void writeNode(Node node, OutputStream output, Map<String, Object> serializerParams) {
935 DOMImplementationLS domImplLS = getLSDOMImpl(node);
936
937 LSSerializer serializer = getLSSerializer(domImplLS, serializerParams);
938
939 LSOutput serializerOut = domImplLS.createLSOutput();
940 serializerOut.setByteStream(output);
941
942 serializer.write(node, serializerOut);
943 }
944
945 /**
946 * Obtain a the DOM, level 3, Load/Save serializer {@link LSSerializer} instance from the
947 * given {@link DOMImplementationLS} instance.
948 *
949 * <p>
950 * The serializer instance will be configured with the parameters passed as the <code>serializerParams</code>
951 * argument. It will also be configured with an {@link LSSerializerFilter} that shows all nodes to the filter,
952 * and accepts all nodes shown.
953 * </p>
954 *
955 * @param domImplLS the DOM Level 3 Load/Save implementation to use
956 * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
957 * instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
958 *
959 * @return a new LSSerializer instance
960 */
961 public static LSSerializer getLSSerializer(DOMImplementationLS domImplLS, Map<String, Object> serializerParams) {
962 LSSerializer serializer = domImplLS.createLSSerializer();
963
964 serializer.setFilter(new LSSerializerFilter() {
965
966 public short acceptNode(Node arg0) {
967 return FILTER_ACCEPT;
968 }
969
970 public int getWhatToShow() {
971 return SHOW_ALL;
972 }
973 });
974
975
976 if (serializerParams != null) {
977 DOMConfiguration serializerDOMConfig = serializer.getDomConfig();
978 for (String key : serializerParams.keySet()) {
979 serializerDOMConfig.setParameter(key, serializerParams.get(key));
980 }
981 }
982
983 return serializer;
984 }
985
986 /**
987 * Get the DOM Level 3 Load/Save {@link DOMImplementationLS} for the given node.
988 *
989 * @param node the node to evaluate
990 * @return the DOMImplementationLS for the given node
991 */
992 public static DOMImplementationLS getLSDOMImpl(Node node) {
993 DOMImplementation domImpl;
994 if (node instanceof Document) {
995 domImpl = ((Document) node).getImplementation();
996 } else {
997 domImpl = node.getOwnerDocument().getImplementation();
998 }
999
1000 DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");
1001 return domImplLS;
1002 }
1003
1004 /**
1005 * Converts a QName into a string that can be used for attribute values or element content.
1006 *
1007 * @param qname the QName to convert to a string
1008 *
1009 * @return the string value of the QName
1010 */
1011 public static String qnameToContentString(QName qname) {
1012 StringBuffer buf = new StringBuffer();
1013
1014 String prefix = DatatypeHelper.safeTrimOrNullString(qname.getPrefix());
1015 if (prefix != null) {
1016 buf.append(prefix);
1017 buf.append(":");
1018 }
1019 buf.append(qname.getLocalPart());
1020 return buf.toString();
1021 }
1022
1023 /**
1024 * Ensures that all the visibly used namespaces referenced by the given Element or its descendants are declared by
1025 * the given Element or one of its descendants.
1026 *
1027 * <strong>NOTE:</strong> This is a very costly operation.
1028 *
1029 * @param domElement the element to act as the root of the namespace declarations
1030 *
1031 * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
1032 */
1033 public static void rootNamespaces(Element domElement) throws XMLParserException {
1034 rootNamespaces(domElement, domElement);
1035 }
1036
1037 /**
1038 * Recursively called function that ensures all the visibly used namespaces referenced by the given Element or its
1039 * descendants are declared if they don't appear in the list of already resolved namespaces.
1040 *
1041 * @param domElement the Element
1042 * @param upperNamespaceSearchBound the "root" element of the fragment where namespaces may be rooted
1043 *
1044 * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
1045 */
1046 private static void rootNamespaces(Element domElement, Element upperNamespaceSearchBound) throws XMLParserException {
1047 String namespaceURI = null;
1048 String namespacePrefix = domElement.getPrefix();
1049
1050 // Check if the namespace for this element is already declared on this element
1051 boolean nsDeclaredOnElement = false;
1052 if (namespacePrefix == null) {
1053 nsDeclaredOnElement = domElement.hasAttributeNS(null, XMLConstants.XMLNS_PREFIX);
1054 } else {
1055 nsDeclaredOnElement = domElement.hasAttributeNS(XMLConstants.XMLNS_NS, namespacePrefix);
1056 }
1057
1058 if (!nsDeclaredOnElement) {
1059 // Namspace for element was not declared on the element itself, see if the namespace is declared on
1060 // an ancestral element within the subtree where namespaces much be rooted
1061 namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
1062
1063 if (namespaceURI == null) {
1064 // Namespace for the element is not declared on any ancestral nodes within the subtree where namespaces
1065 // must be rooted. Resolve the namespace from ancestors outside that subtree.
1066 namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
1067 if (namespaceURI != null) {
1068 // Namespace resolved outside the subtree where namespaces must be declared so declare the namespace
1069 // on this element (within the subtree).
1070 appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
1071 } else {
1072 // Namespace couldn't be resolved from any ancestor. If the namespace prefix is null then the
1073 // element is simply in the undeclared default document namespace, which is fine. If it isn't null
1074 // then a namespace prefix, that hasn't properly been declared, is being used.
1075 if (namespacePrefix != null) {
1076 throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
1077 + " found on element " + getNodeQName(domElement));
1078 }
1079 }
1080 }
1081 }
1082
1083 // Make sure all the attribute URIs are rooted here or have been rooted in an ancestor
1084 NamedNodeMap attributes = domElement.getAttributes();
1085 Node attributeNode;
1086 for (int i = 0; i < attributes.getLength(); i++) {
1087 namespacePrefix = null;
1088 namespaceURI = null;
1089 attributeNode = attributes.item(i);
1090
1091 // Shouldn't need this check, but just to be safe, we have it
1092 if (attributeNode.getNodeType() != Node.ATTRIBUTE_NODE) {
1093 continue;
1094 }
1095
1096 namespacePrefix = attributeNode.getPrefix();
1097 if (!DatatypeHelper.isEmpty(namespacePrefix)) {
1098 // If it's the "xmlns" prefix then it is the namespace declaration,
1099 // don't try to look it up and redeclare it
1100 if (namespacePrefix.equals(XMLConstants.XMLNS_PREFIX)
1101 || namespacePrefix.equals(XMLConstants.XML_PREFIX)) {
1102 continue;
1103 }
1104
1105 // check to see if the namespace for the prefix has already been defined within the XML fragment
1106 namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
1107 if (namespaceURI == null) {
1108 namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
1109 if (namespaceURI == null) {
1110 throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
1111 + " found on attribute " + getNodeQName(attributeNode) + " found on element "
1112 + getNodeQName(domElement));
1113 }
1114
1115 appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
1116 }
1117 }
1118 }
1119
1120 // Now for the child elements, we pass a copy of the resolved namespace list in order to
1121 // maintain proper scoping of namespaces.
1122 NodeList childNodes = domElement.getChildNodes();
1123 Node childNode;
1124 for (int i = 0; i < childNodes.getLength(); i++) {
1125 childNode = childNodes.item(i);
1126 if (childNode.getNodeType() == Node.ELEMENT_NODE) {
1127 rootNamespaces((Element) childNode, upperNamespaceSearchBound);
1128 }
1129 }
1130 }
1131
1132 /**
1133 * Shortcut for checking a DOM element node's namespace and local name.
1134 *
1135 * @param e An element to compare against
1136 * @param ns An XML namespace to compare
1137 * @param localName A local name to compare
1138 * @return true iff the element's local name and namespace match the parameters
1139 */
1140 public static boolean isElementNamed(Element e, String ns, String localName) {
1141 return e != null && DatatypeHelper.safeEquals(ns, e.getNamespaceURI())
1142 && DatatypeHelper.safeEquals(localName, e.getLocalName());
1143 }
1144
1145 /**
1146 * Gets the first child Element of the node, skipping any Text nodes such as whitespace.
1147 *
1148 * @param n The parent in which to search for children
1149 * @return The first child Element of n, or null if none
1150 */
1151 public static Element getFirstChildElement(Node n) {
1152 Node child = n.getFirstChild();
1153 while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
1154 child = child.getNextSibling();
1155 }
1156
1157 if (child != null) {
1158 return (Element) child;
1159 } else {
1160 return null;
1161 }
1162 }
1163
1164 /**
1165 * Gets the next sibling Element of the node, skipping any Text nodes such as whitespace.
1166 *
1167 * @param n The sibling to start with
1168 * @return The next sibling Element of n, or null if none
1169 */
1170 public static Element getNextSiblingElement(Node n) {
1171 Node sib = n.getNextSibling();
1172 while (sib != null && sib.getNodeType() != Node.ELEMENT_NODE) {
1173 sib = sib.getNextSibling();
1174 }
1175
1176 if (sib != null) {
1177 return (Element) sib;
1178 } else {
1179 return null;
1180 }
1181 }
1182
1183 /**
1184 * Converts a lexical duration, as defined by XML Schema 1.0, into milliseconds.
1185 *
1186 * @param duration lexical duration representation
1187 *
1188 * @return duration in milliseconds
1189 */
1190 public static long durationToLong(String duration) {
1191 Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1192 return xmlDuration.getTimeInMillis(new GregorianCalendar());
1193 }
1194
1195 /**
1196 * Converts a duration in milliseconds to a lexical duration, as defined by XML Schema 1.0.
1197 *
1198 * @param duration the duration
1199 *
1200 * @return the lexical representation
1201 */
1202 public static String longToDuration(long duration) {
1203 Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1204 return xmlDuration.toString();
1205 }
1206
1207 }