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