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