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.Collections;
23  import java.util.GregorianCalendar;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.StringTokenizer;
29  import java.util.Map.Entry;
30  
31  import javax.xml.datatype.DatatypeConfigurationException;
32  import javax.xml.datatype.DatatypeFactory;
33  import javax.xml.datatype.Duration;
34  import javax.xml.namespace.QName;
35  import javax.xml.transform.OutputKeys;
36  import javax.xml.transform.Transformer;
37  import javax.xml.transform.TransformerException;
38  import javax.xml.transform.TransformerFactory;
39  import javax.xml.transform.dom.DOMSource;
40  import javax.xml.transform.stream.StreamResult;
41  
42  import org.opensaml.xml.Configuration;
43  import org.opensaml.xml.XMLObject;
44  import org.opensaml.xml.XMLRuntimeException;
45  import org.opensaml.xml.parse.XMLParserException;
46  import org.w3c.dom.Attr;
47  import org.w3c.dom.DOMImplementation;
48  import org.w3c.dom.Document;
49  import org.w3c.dom.Element;
50  import org.w3c.dom.NamedNodeMap;
51  import org.w3c.dom.Node;
52  import org.w3c.dom.NodeList;
53  import org.w3c.dom.Text;
54  import org.w3c.dom.ls.DOMImplementationLS;
55  import org.w3c.dom.ls.LSOutput;
56  import org.w3c.dom.ls.LSSerializer;
57  
58  /**
59   * A helper class for working with W3C DOM objects.
60   */
61  public final class XMLHelper {
62  
63      /**
64       * A string which contains the valid delimiters for the XML Schema 'list' type. These are: space, newline, carriage
65       * return, and tab.
66       */
67      public static final String LIST_DELIMITERS = " \n\r\t";
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         // This results in xmlns="" being emitted, which seems wrong.
579         if (nsURI == null && nsPrefix == null) {
580             return;
581         }
582 
583         String attributeName;
584         if (nsPrefix == null) {
585             attributeName = XMLConstants.XMLNS_PREFIX;
586         } else {
587             attributeName = XMLConstants.XMLNS_PREFIX + ":" + nsPrefix;
588         }
589 
590         String attributeValue;
591         if (nsURI == null) {
592             attributeValue = "";
593         } else {
594             attributeValue = nsURI;
595         }
596 
597         domElement.setAttributeNS(XMLConstants.XMLNS_NS, attributeName, attributeValue);
598     }
599 
600     /**
601      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
602      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
603      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
604      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
605      * doesn't have an namespace delcaration attribute.
606      * 
607      * @param startingElement the starting element
608      * @param prefix the prefix to look up
609      * 
610      * @return the namespace URI for the given prefix
611      */
612     public static String lookupNamespaceURI(Element startingElement, String prefix) {
613         return lookupNamespaceURI(startingElement, null, prefix);
614     }
615 
616     /**
617      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
618      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
619      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
620      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
621      * doesn't have an namespace delcaration attribute.
622      * 
623      * @param startingElement the starting element
624      * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the search
625      * @param prefix the prefix to look up
626      * 
627      * @return the namespace URI for the given prefer or null
628      */
629     public static String lookupNamespaceURI(Element startingElement, Element stopingElement, String prefix) {
630         String namespaceURI;
631 
632         // This code is a modified version of the lookup code within Xerces
633         if (startingElement.hasAttributes()) {
634             NamedNodeMap map = startingElement.getAttributes();
635             int length = map.getLength();
636             for (int i = 0; i < length; i++) {
637                 Node attr = map.item(i);
638                 String attrPrefix = attr.getPrefix();
639                 String value = attr.getNodeValue();
640                 namespaceURI = attr.getNamespaceURI();
641                 if (namespaceURI != null && namespaceURI.equals(XMLConstants.XMLNS_NS)) {
642                     // at this point we are dealing with DOM Level 2 nodes only
643                     if (prefix == null && attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)) {
644                         // default namespace
645                         return value;
646                     } else if (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX)
647                             && attr.getLocalName().equals(prefix)) {
648                         // non default namespace
649                         return value;
650                     }
651                 }
652             }
653         }
654 
655         if(startingElement != stopingElement){
656             Element ancestor = getElementAncestor(startingElement);
657             if (ancestor != null) {
658                 return lookupNamespaceURI(ancestor, stopingElement, prefix);
659             }
660         }
661 
662         return null;
663     }
664 
665     /**
666      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
667      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
668      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
669      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
670      * doesn't have an namespace delcaration attribute.
671      * 
672      * @param startingElement the starting element
673      * @param namespaceURI the uri to look up
674      * 
675      * @return the prefix for the given namespace URI
676      */
677     public static String lookupPrefix(Element startingElement, String namespaceURI) {
678         return lookupPrefix(startingElement, null, namespaceURI);
679     }
680 
681     /**
682      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
683      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
684      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
685      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
686      * doesn't have an namespace delcaration attribute.
687      * 
688      * @param startingElement the starting element
689      * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the search
690      * @param namespaceURI the uri to look up
691      * 
692      * @return the prefix for the given namespace URI
693      */
694     public static String lookupPrefix(Element startingElement, Element stopingElement, String namespaceURI) {
695         String namespace;
696 
697         // This code is a modified version of the lookup code within Xerces
698         if (startingElement.hasAttributes()) {
699             NamedNodeMap map = startingElement.getAttributes();
700             int length = map.getLength();
701             for (int i = 0; i < length; i++) {
702                 Node attr = map.item(i);
703                 String attrPrefix = attr.getPrefix();
704                 String value = attr.getNodeValue();
705                 namespace = attr.getNamespaceURI();
706                 if (namespace != null && namespace.equals(XMLConstants.XMLNS_NS)) {
707                     // DOM Level 2 nodes
708                     if (attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)
709                             || (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX))
710                             && value.equals(namespaceURI)) {
711 
712                         String localname = attr.getLocalName();
713                         String foundNamespace = startingElement.lookupNamespaceURI(localname);
714                         if (foundNamespace != null && foundNamespace.equals(namespaceURI)) {
715                             return localname;
716                         }
717                     }
718 
719                 }
720             }
721         }
722         
723         if(startingElement != stopingElement){
724             Element ancestor = getElementAncestor(startingElement);
725             if (ancestor != null) {
726                 return lookupPrefix(ancestor, stopingElement, namespaceURI);
727             }
728         }
729         
730         return null;
731     }
732 
733     /**
734      * Gets the child nodes with the given namespace qualified tag name. If you need to retrieve multiple, named,
735      * children consider using {@link #getChildElements(Element)}.
736      * 
737      * @param root element to retrieve the children from
738      * @param namespaceURI namespace URI of the child element
739      * @param localName local, tag, name of the child element
740      * 
741      * @return list of child elements, never null
742      */
743     public static List<Element> getChildElementsByTagNameNS(Element root, String namespaceURI, String localName) {
744         ArrayList<Element> children = new ArrayList<Element>();
745         NodeList childNodes = root.getChildNodes();
746 
747         int numOfNodes = childNodes.getLength();
748         Node childNode;
749         Element e;
750         for (int i = 0; i < numOfNodes; i++) {
751             childNode = childNodes.item(i);
752             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
753                 e = (Element) childNode;
754                 if (e.getNamespaceURI().equals(namespaceURI) && e.getLocalName().equals(localName)) {
755                     children.add(e);
756                 }
757             }
758         }
759 
760         return children;
761     }
762 
763     /**
764      * Gets the child nodes with the given local tag name. If you need to retrieve multiple, named, children consider
765      * using {@link #getChildElements(Element)}.
766      * 
767      * @param root element to retrieve the children from
768      * @param localName local, tag, name of the child element
769      * 
770      * @return list of child elements, never null
771      */
772     public static List<Element> getChildElementsByTagName(Element root, String localName) {
773         ArrayList<Element> children = new ArrayList<Element>();
774         NodeList childNodes = root.getChildNodes();
775 
776         int numOfNodes = childNodes.getLength();
777         Node childNode;
778         Element e;
779         for (int i = 0; i < numOfNodes; i++) {
780             childNode = childNodes.item(i);
781             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
782                 e = (Element) childNode;
783                 if (e.getLocalName().equals(localName)) {
784                     children.add(e);
785                 }
786             }
787         }
788 
789         return children;
790     }
791 
792     /**
793      * Gets the child elements of the given element in a single iteration.
794      * 
795      * @param root element to get the child elements of
796      * 
797      * @return child elements indexed by namespace qualifed tag name, never null
798      */
799     public static Map<QName, List<Element>> getChildElements(Element root) {
800         Map<QName, List<Element>> children = new HashMap<QName, List<Element>>();
801         NodeList childNodes = root.getChildNodes();
802 
803         int numOfNodes = childNodes.getLength();
804         Node childNode;
805         Element e;
806         QName qname;
807         List<Element> elements;
808         for (int i = 0; i < numOfNodes; i++) {
809             childNode = childNodes.item(i);
810             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
811                 e = (Element) childNode;
812                 qname = getNodeQName(e);
813                 elements = children.get(qname);
814                 if (elements == null) {
815                     elements = new ArrayList<Element>();
816                     children.put(qname, elements);
817                 }
818 
819                 elements.add(e);
820             }
821         }
822 
823         return children;
824     }
825 
826     /**
827      * Gets the ancestor element node to the given node.
828      * 
829      * @param currentNode the node to retrive the ancestor for
830      * 
831      * @return the ancestral element node of the current node, or null
832      */
833     public static Element getElementAncestor(Node currentNode) {
834         Node parent = currentNode.getParentNode();
835         if (parent != null) {
836             short type = parent.getNodeType();
837             if (type == Node.ELEMENT_NODE) {
838                 return (Element) parent;
839             }
840             return getElementAncestor(parent);
841         }
842         return null;
843     }
844 
845     /**
846      * Converts a Node into a String using the DOM, level 3, Load/Save serializer.
847      * 
848      * @param node the node to be written to a string
849      * 
850      * @return the string representation of the node
851      */
852     public static String nodeToString(Node node) {
853         StringWriter writer = new StringWriter();
854         writeNode(node, writer);
855         return writer.toString();
856     }
857 
858     /**
859      * Pretty prints the XML node.
860      * 
861      * @param node xml node to print
862      * 
863      * @return pretty-printed xml
864      */
865     public static String prettyPrintXML(Node node) {
866         TransformerFactory tfactory = TransformerFactory.newInstance();
867         Transformer serializer;
868         try {
869             serializer = tfactory.newTransformer();
870             serializer.setOutputProperty(OutputKeys.INDENT, "yes");
871             serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");
872 
873             StringWriter output = new StringWriter();
874             serializer.transform(new DOMSource(node), new StreamResult(output));
875             return output.toString();
876         } catch (TransformerException e) {
877             // this is fatal, just dump the stack and throw a runtime exception
878             e.printStackTrace();
879 
880             throw new RuntimeException(e);
881         }
882     }
883 
884     /**
885      * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The writen content is encoded using
886      * the encoding specified in the writer configuration.
887      * 
888      * @param node the node to write out
889      * @param output the writer to write the XML to
890      */
891     public static void writeNode(Node node, Writer output) {
892         DOMImplementation domImpl = node.getOwnerDocument().getImplementation();
893         DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");
894         LSSerializer serializer = domImplLS.createLSSerializer();
895 
896         LSOutput serializerOut = domImplLS.createLSOutput();
897         serializerOut.setCharacterStream(output);
898 
899         serializer.write(node, serializerOut);
900     }
901 
902     /**
903      * Converts a QName into a string that can be used for attribute values or element content.
904      * 
905      * @param qname the QName to convert to a string
906      * 
907      * @return the string value of the QName
908      */
909     public static String qnameToContentString(QName qname) {
910         StringBuffer buf = new StringBuffer();
911 
912         if (qname.getPrefix() != null) {
913             buf.append(qname.getPrefix());
914             buf.append(":");
915         }
916         buf.append(qname.getLocalPart());
917         return buf.toString();
918     }
919 
920     /**
921      * Ensures that all the visibly used namespaces referenced by the given Element or its descendants are declared by
922      * the given Element or one of its descendants.
923      * 
924      * <strong>NOTE:</strong> This is a very costly operation.
925      * 
926      * @param domElement the element to act as the root of the namespace declarations
927      * 
928      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
929      */
930     public static void rootNamespaces(Element domElement) throws XMLParserException {
931         rootNamespaces(domElement, domElement);
932     }
933 
934     /**
935      * Recursively called function that ensures all the visibly used namespaces referenced by the given Element or its
936      * descendants are declared if they don't appear in the list of already resolved namespaces.
937      * 
938      * @param domElement the Element
939      * @param upperNamespaceSearchBound the "root" element of the fragment where namespaces may be rooted
940      * 
941      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
942      */
943     private static void rootNamespaces(Element domElement, Element upperNamespaceSearchBound) throws XMLParserException {
944         String namespaceURI = null;
945         String namespacePrefix = domElement.getPrefix();
946 
947         // Check if the namespace for this element is already declared on this element
948         boolean nsDeclaredOnElement = false;
949         if (namespacePrefix == null) {
950             nsDeclaredOnElement = domElement.hasAttributeNS(null, XMLConstants.XMLNS_PREFIX);
951         } else {
952             nsDeclaredOnElement = domElement.hasAttributeNS(XMLConstants.XMLNS_NS, namespacePrefix);
953         }
954 
955         if (!nsDeclaredOnElement) {
956             // Namspace for element was not declared on the element itself, see if the namespace is declared on
957             // an ancestral element within the subtree where namespaces much be rooted
958             namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
959 
960             if (namespaceURI == null) {
961                 // Namespace for the element is not declared on any ancestral nodes within the subtree where namespaces
962                 // must be rooted. Resolve the namespace from ancestors outside that subtree.
963                 namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
964                 if (namespaceURI != null) {
965                     // Namespace resolved outside the subtree where namespaces must be declared so declare the namespace
966                     // on this element (within the subtree).
967                     appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
968                 } else {
969                     // Namespace couldn't be resolved from any ancestor. If the namespace prefix is null then the
970                     // element is simply in the undeclared default document namespace, which is fine. If it isn't null 
971                     // then a namespace prefix, that hasn't properly been declared, is being used.
972                     if (namespacePrefix != null) {
973                         throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
974                                 + " found on element " + getNodeQName(domElement));
975                     }
976                 }
977             }
978         }
979 
980         // Make sure all the attribute URIs are rooted here or have been rooted in an ancestor
981         NamedNodeMap attributes = domElement.getAttributes();
982         Node attributeNode;
983         for (int i = 0; i < attributes.getLength(); i++) {
984             namespacePrefix = null;
985             namespaceURI = null;
986             attributeNode = attributes.item(i);
987 
988             // Shouldn't need this check, but just to be safe, we have it
989             if (attributeNode.getNodeType() != Node.ATTRIBUTE_NODE) {
990                 continue;
991             }
992 
993             namespacePrefix = attributeNode.getPrefix();
994             if (!DatatypeHelper.isEmpty(namespacePrefix)) {
995                 // If it's the "xmlns" prefix then it is the namespace declaration,
996                 // don't try to look it up and redeclare it
997                 if (namespacePrefix.equals(XMLConstants.XMLNS_PREFIX)
998                         || namespacePrefix.equals(XMLConstants.XML_PREFIX)) {
999                     continue;
1000                 }
1001 
1002                 // check to see if the namespace for the prefix has already been defined within the XML fragment
1003                 namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
1004                 if (namespaceURI == null) {
1005                     namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
1006                     if (namespaceURI == null) {
1007                         throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
1008                                 + " found on attribute " + getNodeQName(attributeNode) + " found on element "
1009                                 + getNodeQName(domElement));
1010                     }
1011 
1012                     appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
1013                 }
1014             }
1015         }
1016 
1017         // Now for the child elements, we pass a copy of the resolved namespace list in order to
1018         // maintain proper scoping of namespaces.
1019         NodeList childNodes = domElement.getChildNodes();
1020         Node childNode;
1021         for (int i = 0; i < childNodes.getLength(); i++) {
1022             childNode = childNodes.item(i);
1023             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
1024                 rootNamespaces((Element) childNode, upperNamespaceSearchBound);
1025             }
1026         }
1027     }
1028 
1029     /**
1030      * Shortcut for checking a DOM element node's namespace and local name.
1031      * 
1032      * @param e An element to compare against
1033      * @param ns An XML namespace to compare
1034      * @param localName A local name to compare
1035      * @return true iff the element's local name and namespace match the parameters
1036      */
1037     public static boolean isElementNamed(Element e, String ns, String localName) {
1038         return e != null && DatatypeHelper.safeEquals(ns, e.getNamespaceURI())
1039                 && DatatypeHelper.safeEquals(localName, e.getLocalName());
1040     }
1041 
1042     /**
1043      * Gets the first child Element of the node, skipping any Text nodes such as whitespace.
1044      * 
1045      * @param n The parent in which to search for children
1046      * @return The first child Element of n, or null if none
1047      */
1048     public static Element getFirstChildElement(Node n) {
1049         Node child = n.getFirstChild();
1050         while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
1051             child = child.getNextSibling();
1052         }
1053 
1054         if (child != null) {
1055             return (Element) child;
1056         } else {
1057             return null;
1058         }
1059     }
1060 
1061     /**
1062      * Gets the next sibling Element of the node, skipping any Text nodes such as whitespace.
1063      * 
1064      * @param n The sibling to start with
1065      * @return The next sibling Element of n, or null if none
1066      */
1067     public static Element getNextSiblingElement(Node n) {
1068         Node sib = n.getNextSibling();
1069         while (sib != null && sib.getNodeType() != Node.ELEMENT_NODE) {
1070             sib = sib.getNextSibling();
1071         }
1072 
1073         if (sib != null) {
1074             return (Element) sib;
1075         } else {
1076             return null;
1077         }
1078     }
1079 
1080     /**
1081      * Converts a lexical duration, as defined by XML Schema 1.0, into milliseconds.
1082      * 
1083      * @param duration lexical duration representation
1084      * 
1085      * @return duration in milliseconds
1086      */
1087     public static long durationToLong(String duration) {
1088         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1089         return xmlDuration.getTimeInMillis(new GregorianCalendar());
1090     }
1091 
1092     /**
1093      * Converts a duration in milliseconds to a lexical duration, as defined by XML Schema 1.0.
1094      * 
1095      * @param duration the duration
1096      * 
1097      * @return the lexical representation
1098      */
1099     public static String longToDuration(long duration) {
1100         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1101         return xmlDuration.toString();
1102     }
1103 
1104 }