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