View Javadoc

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.StringWriter;
20  import java.io.Writer;
21  import java.util.ArrayList;
22  import java.util.GregorianCalendar;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.StringTokenizer;
28  
29  import javax.xml.datatype.DatatypeConfigurationException;
30  import javax.xml.datatype.DatatypeFactory;
31  import javax.xml.datatype.Duration;
32  import javax.xml.namespace.QName;
33  import javax.xml.transform.OutputKeys;
34  import javax.xml.transform.Transformer;
35  import javax.xml.transform.TransformerException;
36  import javax.xml.transform.TransformerFactory;
37  import javax.xml.transform.dom.DOMSource;
38  import javax.xml.transform.stream.StreamResult;
39  
40  import org.opensaml.xml.XMLObject;
41  import org.opensaml.xml.parse.XMLParserException;
42  import org.w3c.dom.Attr;
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  
54  /**
55   * A helper class for working with W3C DOM objects.
56   */
57  public final class XMLHelper {
58  
59      /** JAXP DatatypeFactory. */
60      private static DatatypeFactory dataTypeFactory;
61  
62      /** Constructor. */
63      private XMLHelper() {
64  
65      }
66  
67      /**
68       * Gets a static instance of a JAXP DatatypeFactory.
69       * 
70       * @return the factory or null if the factory could not be created
71       */
72      public static DatatypeFactory getDataTypeFactory() {
73          if (dataTypeFactory == null) {
74              try {
75                  dataTypeFactory = DatatypeFactory.newInstance();
76              } catch (DatatypeConfigurationException e) {
77                  // do nothing
78              }
79          }
80  
81          return dataTypeFactory;
82      }
83  
84      /**
85       * Checks if the given element has an xsi:type defined for it.
86       * 
87       * @param e the DOM element
88       * 
89       * @return true if there is a type, false if not
90       */
91      public static boolean hasXSIType(Element e) {
92          if (e != null) {
93              if (e.getAttributeNodeNS(XMLConstants.XSI_NS, "type") != null) {
94                  return true;
95              }
96          }
97  
98          return false;
99      }
100 
101     /**
102      * Gets the XSI type for a given element if it has one.
103      * 
104      * @param e the element
105      * 
106      * @return the type or null
107      */
108     public static QName getXSIType(Element e) {
109         if (hasXSIType(e)) {
110             Attr attribute = e.getAttributeNodeNS(XMLConstants.XSI_NS, "type");
111             String attributeValue = attribute.getTextContent().trim();
112             StringTokenizer tokenizer = new StringTokenizer(attributeValue, ":");
113             String prefix = null;
114             String localPart;
115             if (tokenizer.countTokens() > 1) {
116                 prefix = tokenizer.nextToken();
117                 localPart = tokenizer.nextToken();
118             } else {
119                 localPart = tokenizer.nextToken();
120             }
121 
122             return constructQName(e.lookupNamespaceURI(prefix), localPart, prefix);
123         }
124 
125         return null;
126     }
127 
128     /**
129      * Gets the ID attribute of a DOM element.
130      * 
131      * @param domElement the DOM element
132      * 
133      * @return the ID attribute or null if there isn't one
134      */
135     public static Attr getIdAttribute(Element domElement) {
136         if (!domElement.hasAttributes()) {
137             return null;
138         }
139 
140         NamedNodeMap attributes = domElement.getAttributes();
141         Attr attribute;
142         for (int i = 0; i < attributes.getLength(); i++) {
143             attribute = (Attr) attributes.item(i);
144             if (attribute.isId()) {
145                 return attribute;
146             }
147         }
148 
149         return null;
150     }
151 
152     /**
153      * Gets the QName for the given DOM node.
154      * 
155      * @param domNode the DOM node
156      * 
157      * @return the QName for the element or null if the element was null
158      */
159     public static QName getNodeQName(Node domNode) {
160         if (domNode != null) {
161             return constructQName(domNode.getNamespaceURI(), domNode.getLocalName(), domNode.getPrefix());
162         }
163 
164         return null;
165     }
166     
167     /**
168      * Gets the lcoale currently active for the element.  This is done by looking for an xml:lang attribute 
169      * and parsing its content.  If no xml:lang attribute is present the default locale is returned.  This method 
170      * only uses the language primary tag, as defined by RFC3066.
171      * 
172      * @param element element to retrieve local information for
173      * 
174      * @return the active local of the element
175      */
176     public static Locale getLanguage(Element element) {
177         String lang = DatatypeHelper.safeTrimOrNullString(element.getAttributeNS(XMLConstants.XML_NS, "lang"));
178         if(lang != null){
179             if(lang.contains("-")){
180                 lang = lang.substring(0, lang.indexOf("-"));
181             }
182             return new Locale(lang.toUpperCase());
183         }else{
184             return Locale.getDefault();
185         }
186     }
187 
188     /**
189      * Constructs an attribute owned by the given document with the given name.
190      * 
191      * @param owningDocument the owning document
192      * @param attributeName the name of that attribute
193      * 
194      * @return the constructed attribute
195      */
196     public static Attr constructAttribute(Document owningDocument, QName attributeName) {
197         return constructAttribute(owningDocument, attributeName.getNamespaceURI(), attributeName.getLocalPart(),
198                 attributeName.getPrefix());
199     }
200 
201     /**
202      * Constructs an attribute owned by the given document with the given name.
203      * 
204      * @param document the owning document
205      * @param namespaceURI the URI for the namespace the attribute is in
206      * @param localName the local name
207      * @param prefix the prefix of the namespace that attribute is in
208      * 
209      * @return the constructed attribute
210      */
211     public static Attr constructAttribute(Document document, String namespaceURI, String localName, String prefix) {
212         String trimmedLocalName = DatatypeHelper.safeTrimOrNullString(localName);
213 
214         if (trimmedLocalName == null) {
215             throw new IllegalArgumentException("Local name may not be null or empty");
216         }
217 
218         String qualifiedName;
219         String trimmedPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
220         if (trimmedPrefix != null) {
221             qualifiedName = trimmedPrefix + ":" + DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
222         } else {
223             qualifiedName = DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
224         }
225 
226         if (DatatypeHelper.isEmpty(namespaceURI)) {
227             return document.createAttribute(qualifiedName);
228         } else {
229             return document.createAttributeNS(namespaceURI, qualifiedName);
230         }
231     }
232 
233     /**
234      * Constructs a QName from an attributes value.
235      * 
236      * @param attribute the attribute with a QName value
237      * 
238      * @return a QName from an attributes value, or null if the given attribute is null
239      */
240     public static QName getAttributeValueAsQName(Attr attribute) {
241         if (attribute == null || DatatypeHelper.isEmpty(attribute.getValue())) {
242             return null;
243         }
244 
245         String attributeValue = attribute.getTextContent();
246         String[] valueComponents = attributeValue.split(":");
247         if (valueComponents.length == 1) {
248             return constructQName(attribute.lookupNamespaceURI(null), valueComponents[0], null);
249         } else {
250             return constructQName(attribute.lookupNamespaceURI(valueComponents[0]), valueComponents[1],
251                     valueComponents[0]);
252         }
253     }
254 
255     /**
256      * Parses the attribute's value. If the value is 0 or "false" then false is returned, if the value is 1 or "true"
257      * then true is returned, if the value is anything else then null returned.
258      * 
259      * @param attribute attribute whose value will be converted to a boolean
260      * 
261      * @return boolean value of the attribute or null
262      */
263     public static Boolean getAttributeValueAsBoolean(Attr attribute) {
264         if(attribute == null){
265             return null;
266         }
267         
268         String valueStr = attribute.getValue();
269         if (valueStr.equals("0") || valueStr.equals("false")) {
270             return Boolean.FALSE;
271         } else if (valueStr.equals("1") || valueStr.equals("true")) {
272             return Boolean.TRUE;
273         } else {
274             return null;
275         }
276     }
277 
278     /**
279      * Gets the value of a list-type attribute as a list.
280      * 
281      * @param attribute attribute whose value will be turned into a list
282      * 
283      * @return list of values, never null
284      */
285     public static List<String> getAttributeValueAsList(Attr attribute) {
286         ArrayList<String> values = new ArrayList<String>();
287         if (attribute == null) {
288             return values;
289         }
290 
291         StringTokenizer valueTokens = new StringTokenizer(DatatypeHelper.safeTrimOrNullString(attribute.getValue()));
292         while (valueTokens.hasMoreTokens()) {
293             values.add(valueTokens.nextToken());
294         }
295 
296         return values;
297     }
298 
299     /**
300      * Constructs a QName from an element's adjacent Text child nodes.
301      * 
302      * @param element the element with a QName value
303      * 
304      * @return a QName from an element's value, or null if the given element is empty
305      */
306     public static QName getElementContentAsQName(Element element) {
307         if (element == null) {
308             return null;
309         }
310 
311         String elementContent = null;
312         NodeList nodeList = element.getChildNodes();
313         for (int i = 0; i < nodeList.getLength(); i++) {
314             Node node = nodeList.item(i);
315             if (node.getNodeType() == Node.TEXT_NODE) {
316                 elementContent = DatatypeHelper.safeTrimOrNullString(((Text) node).getWholeText());
317                 break;
318             }
319         }
320 
321         if (elementContent == null) {
322             return null;
323         }
324 
325         String[] valueComponents = elementContent.split(":");
326         if (valueComponents.length == 1) {
327             return constructQName(element.lookupNamespaceURI(null), valueComponents[0], null);
328         } else {
329             return constructQName(element.lookupNamespaceURI(valueComponents[0]), valueComponents[1],
330                     valueComponents[0]);
331         }
332     }
333 
334     /**
335      * Gets the value of a list-type element as a list.
336      * 
337      * @param element element whose value will be turned into a list
338      * 
339      * @return list of values, never null
340      */
341     public static List<String> getElementContentAsList(Element element) {
342         ArrayList<String> values = new ArrayList<String>();
343         if (element == null) {
344             return values;
345         }
346 
347         String elementContent = DatatypeHelper.safeTrimOrNullString(element.getTextContent());
348         StringTokenizer valueTokens = new StringTokenizer(elementContent);
349         while (valueTokens.hasMoreTokens()) {
350             values.add(valueTokens.nextToken());
351         }
352 
353         return values;
354     }
355 
356     /**
357      * Constructs a QName.
358      * 
359      * @param namespaceURI the namespace of the QName
360      * @param localName the local name of the QName
361      * @param prefix the prefix of the QName, may be null
362      * 
363      * @return the QName
364      */
365     public static QName constructQName(String namespaceURI, String localName, String prefix) {
366         if (DatatypeHelper.isEmpty(prefix)) {
367             return new QName(namespaceURI, localName);
368         } else if (DatatypeHelper.isEmpty(namespaceURI)) {
369             return new QName(localName);
370         }
371 
372         return new QName(namespaceURI, localName, prefix);
373     }
374 
375     /**
376      * Constructs a QName from a string (attribute or element content) value.
377      * 
378      * @param qname the QName string
379      * @param owningObject XMLObject, with cached DOM, owning the QName
380      * 
381      * @return the QName respresented by the string
382      */
383     public static QName constructQName(String qname, XMLObject owningObject) {
384         return constructQName(qname, owningObject.getDOM());
385     }
386     
387     /**
388      * Constructs a QName from a string (attribute element content) value.
389      * 
390      * @param qname the QName string
391      * @param owningElement parent DOM element of the Node which contains the QName value
392      * 
393      * @return the QName respresented by the string
394      */
395     public static QName constructQName(String qname, Element owningElement) {
396         String nsURI;
397         String nsPrefix;
398         String name;
399 
400         if (qname.indexOf(":") > -1) {
401             StringTokenizer qnameTokens = new StringTokenizer(qname, ":");
402             nsPrefix = qnameTokens.nextToken();
403             name = qnameTokens.nextToken();
404         } else {
405             nsPrefix = "";
406             name = qname;
407         }
408 
409         nsURI = lookupNamespaceURI(owningElement, nsPrefix);
410         return constructQName(nsURI, name, nsPrefix);
411     }
412 
413     /**
414      * Constructs an element, rooted in the given document, with the given name.
415      * 
416      * @param document the document containing the element
417      * @param elementName the name of the element, must contain a local name, may contain a namespace URI and prefix
418      * 
419      * @return the element
420      */
421     public static Element constructElement(Document document, QName elementName) {
422         return constructElement(document, elementName.getNamespaceURI(), elementName.getLocalPart(), elementName
423                 .getPrefix());
424     }
425 
426     /**
427      * Constructs an element, rooted in the given document, with the given information.
428      * 
429      * @param document the document containing the element
430      * @param namespaceURI the URI of the namespace the element is in
431      * @param localName the element's local name
432      * @param prefix the prefix of the namespace the element is in
433      * 
434      * @return the element
435      */
436     public static Element constructElement(Document document, String namespaceURI, String localName, String prefix) {
437         String trimmedLocalName = DatatypeHelper.safeTrimOrNullString(localName);
438 
439         if (trimmedLocalName == null) {
440             throw new IllegalArgumentException("Local name may not be null or empty");
441         }
442 
443         String qualifiedName;
444         String trimmedPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
445         if (trimmedPrefix != null) {
446             qualifiedName = trimmedPrefix + ":" + DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
447         } else {
448             qualifiedName = DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
449         }
450 
451         if (!DatatypeHelper.isEmpty(namespaceURI)) {
452             return document.createElementNS(namespaceURI, qualifiedName);
453         } else {
454             return document.createElementNS(null, qualifiedName);
455         }
456     }
457 
458     /**
459      * Appends the child Element to the parent Element, adopting the child Element into the parent's Document if needed.
460      * 
461      * @param parentElement the parent Element
462      * @param childElement the child Element
463      */
464     public static void appendChildElement(Element parentElement, Element childElement) {
465         Document parentDocument = parentElement.getOwnerDocument();
466         adoptElement(childElement, parentDocument);
467 
468         parentElement.appendChild(childElement);
469     }
470 
471     /**
472      * Adopts an element into a document if the child is not already in the document.
473      * 
474      * @param adoptee the element to be adopted
475      * @param adopter the document into which the element is adopted
476      */
477     public static void adoptElement(Element adoptee, Document adopter) {
478         if (!(adoptee.getOwnerDocument().equals(adopter))) {
479             adopter.adoptNode(adoptee);
480         }
481     }
482 
483     /**
484      * Creates a text node with the given content and appends it as child to the given element.
485      * 
486      * @param domElement the element to recieve the text node
487      * @param textContent the content for the text node
488      */
489     public static void appendTextContent(Element domElement, String textContent) {
490         if (textContent == null) {
491             return;
492         }
493         Document parentDocument = domElement.getOwnerDocument();
494         Text textNode = parentDocument.createTextNode(textContent);
495         domElement.appendChild(textNode);
496     }
497 
498     /**
499      * Adds a namespace declaration (xmlns:) attribute to the given element.
500      * 
501      * @param domElement the element to add the attribute to
502      * @param namespaceURI the URI of the namespace
503      * @param prefix the prefix for the namespace
504      */
505     public static void appendNamespaceDeclaration(Element domElement, String namespaceURI, String prefix) {
506         String nsURI = DatatypeHelper.safeTrimOrNullString(namespaceURI);
507         String nsPrefix = DatatypeHelper.safeTrimOrNullString(prefix);
508         
509         // This results in xmlns="" being emitted, which seems wrong.
510         if (nsURI == null && nsPrefix == null) {
511             return;
512         }
513 
514         String attributeName;
515         if (nsPrefix == null) {
516             attributeName = XMLConstants.XMLNS_PREFIX;
517         } else {
518             attributeName = XMLConstants.XMLNS_PREFIX + ":" + nsPrefix;
519         }
520 
521         String attributeValue;
522         if (nsURI == null) {
523             attributeValue = "";
524         } else {
525             attributeValue = nsURI;
526         }
527 
528         domElement.setAttributeNS(XMLConstants.XMLNS_NS, attributeName, attributeValue);
529     }
530 
531     /**
532      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
533      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
534      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
535      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
536      * doesn't have an namespace delcaration attribute.
537      * 
538      * @param startingElement the starting element
539      * @param prefix the prefix to look up
540      * 
541      * @return the namespace URI for the given prefix
542      */
543     public static String lookupNamespaceURI(Element startingElement, String prefix) {
544         return lookupNamespaceURI(startingElement, null, prefix);
545     }
546 
547     /**
548      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
549      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
550      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
551      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
552      * doesn't have an namespace delcaration attribute.
553      * 
554      * @param startingElement the starting element
555      * @param stopingElement the ancestor of the starting element that serves as the upper-bound for the search
556      * @param prefix the prefix to look up
557      * 
558      * @return the namespace URI for the given prefer or null
559      */
560     public static String lookupNamespaceURI(Element startingElement, Element stopingElement, String prefix) {
561         String namespaceURI;
562 
563         if (startingElement == stopingElement) {
564             return null;
565         }
566 
567         // This code is a modified version of the lookup code within Xerces
568         if (startingElement.hasAttributes()) {
569             NamedNodeMap map = startingElement.getAttributes();
570             int length = map.getLength();
571             for (int i = 0; i < length; i++) {
572                 Node attr = map.item(i);
573                 String attrPrefix = attr.getPrefix();
574                 String value = attr.getNodeValue();
575                 namespaceURI = attr.getNamespaceURI();
576                 if (namespaceURI != null && namespaceURI.equals(XMLConstants.XMLNS_NS)) {
577                     // at this point we are dealing with DOM Level 2 nodes only
578                     if (prefix == null && attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)) {
579                         // default namespace
580                         return value;
581                     } else if (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX)
582                             && attr.getLocalName().equals(prefix)) {
583                         // non default namespace
584                         return value;
585                     }
586                 }
587             }
588         }
589 
590         Element ancestor = getElementAncestor(startingElement);
591 
592         if (ancestor != null) {
593             return lookupNamespaceURI(ancestor, stopingElement, prefix);
594         }
595 
596         return null;
597     }
598 
599     /**
600      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
601      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
602      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
603      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
604      * doesn't have an namespace delcaration attribute.
605      * 
606      * @param startingElement the starting element
607      * @param namespaceURI the uri to look up
608      * 
609      * @return the prefix for the given namespace URI
610      */
611     public static String lookupPrefix(Element startingElement, String namespaceURI) {
612         return lookupPrefix(startingElement, null, namespaceURI);
613     }
614 
615     /**
616      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
617      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
618      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
619      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
620      * doesn't have an namespace delcaration attribute.
621      * 
622      * @param startingElement the starting element
623      * @param stopingElement the ancestor of the starting element that serves as the upper-bound for the search
624      * @param namespaceURI the uri to look up
625      * 
626      * @return the prefix for the given namespace URI
627      */
628     public static String lookupPrefix(Element startingElement, Element stopingElement, String namespaceURI) {
629         String namespace;
630 
631         if (startingElement == stopingElement) {
632             return null;
633         }
634 
635         // This code is a modified version of the lookup code within Xerces
636         if (startingElement.hasAttributes()) {
637             NamedNodeMap map = startingElement.getAttributes();
638             int length = map.getLength();
639             for (int i = 0; i < length; i++) {
640                 Node attr = map.item(i);
641                 String attrPrefix = attr.getPrefix();
642                 String value = attr.getNodeValue();
643                 namespace = attr.getNamespaceURI();
644                 if (namespace != null && namespace.equals(XMLConstants.XMLNS_NS)) {
645                     // DOM Level 2 nodes
646                     if (attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)
647                             || (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX))
648                             && value.equals(namespaceURI)) {
649 
650                         String localname = attr.getLocalName();
651                         String foundNamespace = startingElement.lookupNamespaceURI(localname);
652                         if (foundNamespace != null && foundNamespace.equals(namespaceURI)) {
653                             return localname;
654                         }
655                     }
656 
657                 }
658             }
659         }
660         Element ancestor = getElementAncestor(startingElement);
661 
662         if (ancestor != null) {
663             return lookupPrefix(ancestor, stopingElement, namespaceURI);
664         }
665         return null;
666     }
667 
668     /**
669      * Gets the child nodes with the given namespace qualified tag name. If you need to retrieve multiple, named,
670      * children consider using {@link #getChildElements(Element)}.
671      * 
672      * @param root element to retrieve the children from
673      * @param namespaceURI namespace URI of the child element
674      * @param localName local, tag, name of the child element
675      * 
676      * @return list of child elements, never null
677      */
678     public static List<Element> getChildElementsByTagNameNS(Element root, String namespaceURI, String localName) {
679         ArrayList<Element> children = new ArrayList<Element>();
680         NodeList childNodes = root.getChildNodes();
681 
682         int numOfNodes = childNodes.getLength();
683         Node childNode;
684         Element e;
685         for (int i = 0; i < numOfNodes; i++) {
686             childNode = childNodes.item(i);
687             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
688                 e = (Element) childNode;
689                 if (e.getNamespaceURI().equals(namespaceURI) && e.getLocalName().equals(localName)) {
690                     children.add(e);
691                 }
692             }
693         }
694 
695         return children;
696     }
697 
698     /**
699      * Gets the child nodes with the given local tag name. If you need to retrieve multiple, named, children consider
700      * using {@link #getChildElements(Element)}.
701      * 
702      * @param root element to retrieve the children from
703      * @param localName local, tag, name of the child element
704      * 
705      * @return list of child elements, never null
706      */
707     public static List<Element> getChildElementsByTagName(Element root, String localName) {
708         ArrayList<Element> children = new ArrayList<Element>();
709         NodeList childNodes = root.getChildNodes();
710 
711         int numOfNodes = childNodes.getLength();
712         Node childNode;
713         Element e;
714         for (int i = 0; i < numOfNodes; i++) {
715             childNode = childNodes.item(i);
716             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
717                 e = (Element) childNode;
718                 if (e.getLocalName().equals(localName)) {
719                     children.add(e);
720                 }
721             }
722         }
723 
724         return children;
725     }
726 
727     /**
728      * Gets the child elements of the given element in a single iteration.
729      * 
730      * @param root element to get the child elements of
731      * 
732      * @return child elements indexed by namespace qualifed tag name, never null
733      */
734     public static Map<QName, List<Element>> getChildElements(Element root) {
735         Map<QName, List<Element>> children = new HashMap<QName, List<Element>>();
736         NodeList childNodes = root.getChildNodes();
737 
738         int numOfNodes = childNodes.getLength();
739         Node childNode;
740         Element e;
741         QName qname;
742         List<Element> elements;
743         for (int i = 0; i < numOfNodes; i++) {
744             childNode = childNodes.item(i);
745             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
746                 e = (Element) childNode;
747                 qname = getNodeQName(e);
748                 elements = children.get(qname);
749                 if (elements == null) {
750                     elements = new ArrayList<Element>();
751                     children.put(qname, elements);
752                 }
753 
754                 elements.add(e);
755             }
756         }
757 
758         return children;
759     }
760 
761     /**
762      * Gets the ancestor element node to the given node.
763      * 
764      * @param currentNode the node to retrive the ancestor for
765      * 
766      * @return the ancestral element node of the current node, or null
767      */
768     public static Element getElementAncestor(Node currentNode) {
769         Node parent = currentNode.getParentNode();
770         if (parent != null) {
771             short type = parent.getNodeType();
772             if (type == Node.ELEMENT_NODE) {
773                 return (Element) parent;
774             }
775             return getElementAncestor(parent);
776         }
777         return null;
778     }
779 
780     /**
781      * Converts a Node into a String using the DOM, level 3, Load/Save serializer.
782      * 
783      * @param node the node to be written to a string
784      * 
785      * @return the string representation of the node
786      */
787     public static String nodeToString(Node node) {
788         StringWriter writer = new StringWriter();
789         writeNode(node, writer);
790         return writer.toString();
791     }
792 
793     /**
794      * Pretty prints the XML node.
795      * 
796      * @param node xml node to print
797      * 
798      * @return pretty-printed xml
799      */
800     public static String prettyPrintXML(Node node) {
801         TransformerFactory tfactory = TransformerFactory.newInstance();
802         Transformer serializer;
803         try {
804             serializer = tfactory.newTransformer();
805             serializer.setOutputProperty(OutputKeys.INDENT, "yes");
806             serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");
807 
808             StringWriter output = new StringWriter();
809             serializer.transform(new DOMSource(node.getOwnerDocument()), new StreamResult(output));
810             return output.toString();
811         } catch (TransformerException e) {
812             // this is fatal, just dump the stack and throw a runtime exception
813             e.printStackTrace();
814 
815             throw new RuntimeException(e);
816         }
817     }
818 
819     /**
820      * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The writen content is encoded using
821      * the encoding specified in the writer configuration.
822      * 
823      * @param node the node to write out
824      * @param output the writer to write the XML to
825      */
826     public static void writeNode(Node node, Writer output) {
827         DOMImplementation domImpl = node.getOwnerDocument().getImplementation();
828         DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");
829         LSSerializer serializer = domImplLS.createLSSerializer();
830 
831         LSOutput serializerOut = domImplLS.createLSOutput();
832         serializerOut.setCharacterStream(output);
833 
834         serializer.write(node, serializerOut);
835     }
836 
837     /**
838      * Converts a QName into a string that can be used for attribute values or element content.
839      * 
840      * @param qname the QName to convert to a string
841      * 
842      * @return the string value of the QName
843      */
844     public static String qnameToContentString(QName qname) {
845         StringBuffer buf = new StringBuffer();
846 
847         if (qname.getPrefix() != null) {
848             buf.append(qname.getPrefix());
849             buf.append(":");
850         }
851         buf.append(qname.getLocalPart());
852         return buf.toString();
853     }
854 
855     /**
856      * Ensures that all the visibly used namespaces referenced by the given Element or its descendants are declared by
857      * the given Element or one of its descendants.
858      * 
859      * <strong>NOTE:</strong> This is a very costly operation.
860      * 
861      * @param domElement the element to act as the root of the namespace declarations
862      * 
863      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
864      */
865     public static void rootNamespaces(Element domElement) throws XMLParserException {
866         rootNamespaces(domElement, domElement);
867     }
868 
869     /**
870      * Recursively called function that ensures all the visibly used namespaces referenced by the given Element or its
871      * descendants are declared if they don't appear in the list of already resolved namespaces.
872      * 
873      * @param domElement the Element
874      * @param upperNamespaceSearchBound the "root" element of the fragment where namespaces may be rooted
875      * 
876      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
877      */
878     private static void rootNamespaces(Element domElement, Element upperNamespaceSearchBound) throws XMLParserException {
879         String namespaceURI = null;
880         String namespacePrefix = null;
881 
882         // Make sure this element's namespace is rooted or has been rooted in an ancestor
883         namespacePrefix = domElement.getPrefix();
884         if (DatatypeHelper.isEmpty(namespacePrefix)) {
885             namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, "");
886         } else {
887             namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
888         }
889 
890         if (namespaceURI == null) {
891             namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
892             if (namespaceURI == null) {
893                 throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
894                         + " found on element " + getNodeQName(domElement));
895             }
896 
897             appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
898         }
899 
900         // Make sure all the attribute URIs are rooted here or have been rooted in an ancestor
901         NamedNodeMap attributes = domElement.getAttributes();
902         Node attributeNode;
903         for (int i = 0; i < attributes.getLength(); i++) {
904             namespacePrefix = null;
905             namespaceURI = null;
906             attributeNode = attributes.item(i);
907 
908             // Shouldn't need this check, but just to be safe, we have it
909             if (attributeNode.getNodeType() != Node.ATTRIBUTE_NODE) {
910                 continue;
911             }
912 
913             namespacePrefix = attributeNode.getPrefix();
914             if (!DatatypeHelper.isEmpty(namespacePrefix)) {
915                 // If it's the "xmlns" prefix then it is the namespace declaration,
916                 // don't try to look it up and redeclare it
917                 if (namespacePrefix.equals(XMLConstants.XMLNS_PREFIX)
918                         || namespacePrefix.equals(XMLConstants.XML_PREFIX)) {
919                     continue;
920                 }
921 
922                 // check to see if the namespace for the prefix has already been defined within the XML fragment
923                 namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
924                 if (namespaceURI == null) {
925                     namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
926                     if (namespaceURI == null) {
927                         throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
928                                 + " found on attribute " + getNodeQName(attributeNode) + " found on element "
929                                 + getNodeQName(domElement));
930                     }
931 
932                     appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
933                 }
934             }
935         }
936 
937         // Now for the child elements, we pass a copy of the resolved namespace list in order to
938         // maintain proper scoping of namespaces.
939         NodeList childNodes = domElement.getChildNodes();
940         Node childNode;
941         for (int i = 0; i < childNodes.getLength(); i++) {
942             childNode = childNodes.item(i);
943             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
944                 rootNamespaces((Element) childNode, upperNamespaceSearchBound);
945             }
946         }
947     }
948 
949     /**
950      * Shortcut for checking a DOM element node's namespace and local name.
951      * 
952      * @param e An element to compare against
953      * @param ns An XML namespace to compare
954      * @param localName A local name to compare
955      * @return true iff the element's local name and namespace match the parameters
956      */
957     public static boolean isElementNamed(Element e, String ns, String localName) {
958         return e != null && DatatypeHelper.safeEquals(ns, e.getNamespaceURI())
959                 && DatatypeHelper.safeEquals(localName, e.getLocalName());
960     }
961 
962     /**
963      * Gets the first child Element of the node, skipping any Text nodes such as whitespace.
964      * 
965      * @param n The parent in which to search for children
966      * @return The first child Element of n, or null if none
967      */
968     public static Element getFirstChildElement(Node n) {
969         Node child = n.getFirstChild();
970         while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
971             child = child.getNextSibling();
972         }
973 
974         if (child != null) {
975             return (Element) child;
976         } else {
977             return null;
978         }
979     }
980 
981     /**
982      * Gets the next sibling Element of the node, skipping any Text nodes such as whitespace.
983      * 
984      * @param n The sibling to start with
985      * @return The next sibling Element of n, or null if none
986      */
987     public static Element getNextSiblingElement(Node n) {
988         Node sib = n.getNextSibling();
989         while (sib != null && sib.getNodeType() != Node.ELEMENT_NODE) {
990             sib = sib.getNextSibling();
991         }
992 
993         if (sib != null) {
994             return (Element) sib;
995         } else {
996             return null;
997         }
998     }
999 
1000     /**
1001      * Converts a lexical duration, as defined by XML Schema 1.0, into milliseconds.
1002      * 
1003      * @param duration lexical duration representation
1004      * 
1005      * @return duration in milliseconds
1006      */
1007     public static long durationToLong(String duration) {
1008         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1009         return xmlDuration.getTimeInMillis(new GregorianCalendar());
1010     }
1011 
1012     /**
1013      * Converts a duration in milliseconds to a lexical duration, as defined by XML Schema 1.0.
1014      * 
1015      * @param duration the duration
1016      * 
1017      * @return the lexical representation
1018      */
1019     public static String longToDuration(long duration) {
1020         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1021         return xmlDuration.toString();
1022     }
1023 }