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         // This results in xmlns="" being emitted, which seems wrong.
579         if (nsURI == null && nsPrefix == null) {
580             return;
581         }
582 
583         String attributeName;
584         if (nsPrefix == null) {
585             attributeName = XMLConstants.XMLNS_PREFIX;
586         } else {
587             attributeName = XMLConstants.XMLNS_PREFIX + ":" + nsPrefix;
588         }
589 
590         String attributeValue;
591         if (nsURI == null) {
592             attributeValue = "";
593         } else {
594             attributeValue = nsURI;
595         }
596 
597         domElement.setAttributeNS(XMLConstants.XMLNS_NS, attributeName, attributeValue);
598     }
599 
600     /**
601      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
602      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
603      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
604      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
605      * doesn't have an namespace delcaration attribute.
606      * 
607      * @param startingElement the starting element
608      * @param prefix the prefix to look up
609      * 
610      * @return the namespace URI for the given prefix
611      */
612     public static String lookupNamespaceURI(Element startingElement, String prefix) {
613         return lookupNamespaceURI(startingElement, null, prefix);
614     }
615 
616     /**
617      * Looks up the namespace URI associated with the given prefix starting at the given element. This method differs
618      * from the {@link Node#lookupNamespaceURI(java.lang.String)} in that it only those namespaces declared by an xmlns
619      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
620      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
621      * doesn't have an namespace delcaration attribute.
622      * 
623      * @param startingElement the starting element
624      * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the
625      *            search
626      * @param prefix the prefix to look up
627      * 
628      * @return the namespace URI for the given prefer or null
629      */
630     public static String lookupNamespaceURI(Element startingElement, Element stopingElement, String prefix) {
631         String namespaceURI;
632 
633         // This code is a modified version of the lookup code within Xerces
634         if (startingElement.hasAttributes()) {
635             NamedNodeMap map = startingElement.getAttributes();
636             int length = map.getLength();
637             for (int i = 0; i < length; i++) {
638                 Node attr = map.item(i);
639                 String attrPrefix = attr.getPrefix();
640                 String value = attr.getNodeValue();
641                 namespaceURI = attr.getNamespaceURI();
642                 if (namespaceURI != null && namespaceURI.equals(XMLConstants.XMLNS_NS)) {
643                     // at this point we are dealing with DOM Level 2 nodes only
644                     if (prefix == null && attr.getNodeName().equals(XMLConstants.XMLNS_PREFIX)) {
645                         // default namespace
646                         return value;
647                     } else if (attrPrefix != null && attrPrefix.equals(XMLConstants.XMLNS_PREFIX)
648                             && attr.getLocalName().equals(prefix)) {
649                         // non default namespace
650                         return value;
651                     }
652                 }
653             }
654         }
655 
656         if (startingElement != stopingElement) {
657             Element ancestor = getElementAncestor(startingElement);
658             if (ancestor != null) {
659                 return lookupNamespaceURI(ancestor, stopingElement, prefix);
660             }
661         }
662 
663         return null;
664     }
665 
666     /**
667      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
668      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
669      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
670      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
671      * doesn't have an namespace delcaration attribute.
672      * 
673      * @param startingElement the starting element
674      * @param namespaceURI the uri to look up
675      * 
676      * @return the prefix for the given namespace URI
677      */
678     public static String lookupPrefix(Element startingElement, String namespaceURI) {
679         return lookupPrefix(startingElement, null, namespaceURI);
680     }
681 
682     /**
683      * Looks up the namespace prefix associated with the given URI starting at the given element. This method differs
684      * from the {@link Node#lookupPrefix(java.lang.String)} in that it only those namespaces declared by an xmlns
685      * attribute are inspected. The Node method also checks the namespace a particular node was created in by way of a
686      * call like {@link Document#createElementNS(java.lang.String, java.lang.String)} even if the resulting element
687      * doesn't have an namespace delcaration attribute.
688      * 
689      * @param startingElement the starting element
690      * @param stopingElement the ancestor of the starting element that serves as the upper-bound, inclusive, for the
691      *            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 (DatatypeHelper.safeEquals(e.getNamespaceURI(), namespaceURI)
757                         && DatatypeHelper.safeEquals(e.getLocalName(), localName)) {
758                     children.add(e);
759                 }
760             }
761         }
762 
763         return children;
764     }
765 
766     /**
767      * Gets the child nodes with the given local tag name. If you need to retrieve multiple, named, children consider
768      * using {@link #getChildElements(Element)}.
769      * 
770      * @param root element to retrieve the children from
771      * @param localName local, tag, name of the child element
772      * 
773      * @return list of child elements, never null
774      */
775     public static List<Element> getChildElementsByTagName(Element root, String localName) {
776         ArrayList<Element> children = new ArrayList<Element>();
777         NodeList childNodes = root.getChildNodes();
778 
779         int numOfNodes = childNodes.getLength();
780         Node childNode;
781         Element e;
782         for (int i = 0; i < numOfNodes; i++) {
783             childNode = childNodes.item(i);
784             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
785                 e = (Element) childNode;
786                 if (DatatypeHelper.safeEquals(e.getLocalName(), localName)) {
787                     children.add(e);
788                 }
789             }
790         }
791 
792         return children;
793     }
794 
795     /**
796      * Gets the child elements of the given element in a single iteration.
797      * 
798      * @param root element to get the child elements of
799      * 
800      * @return child elements indexed by namespace qualifed tag name, never null
801      */
802     public static Map<QName, List<Element>> getChildElements(Element root) {
803         Map<QName, List<Element>> children = new HashMap<QName, List<Element>>();
804         NodeList childNodes = root.getChildNodes();
805 
806         int numOfNodes = childNodes.getLength();
807         Node childNode;
808         Element e;
809         QName qname;
810         List<Element> elements;
811         for (int i = 0; i < numOfNodes; i++) {
812             childNode = childNodes.item(i);
813             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
814                 e = (Element) childNode;
815                 qname = getNodeQName(e);
816                 elements = children.get(qname);
817                 if (elements == null) {
818                     elements = new ArrayList<Element>();
819                     children.put(qname, elements);
820                 }
821 
822                 elements.add(e);
823             }
824         }
825 
826         return children;
827     }
828 
829     /**
830      * Gets the ancestor element node to the given node.
831      * 
832      * @param currentNode the node to retrive the ancestor for
833      * 
834      * @return the ancestral element node of the current node, or null
835      */
836     public static Element getElementAncestor(Node currentNode) {
837         Node parent = currentNode.getParentNode();
838         if (parent != null) {
839             short type = parent.getNodeType();
840             if (type == Node.ELEMENT_NODE) {
841                 return (Element) parent;
842             }
843             return getElementAncestor(parent);
844         }
845         return null;
846     }
847 
848     /**
849      * Converts a Node into a String using the DOM, level 3, Load/Save serializer.
850      * 
851      * @param node the node to be written to a string
852      * 
853      * @return the string representation of the node
854      */
855     public static String nodeToString(Node node) {
856         StringWriter writer = new StringWriter();
857         writeNode(node, writer);
858         return writer.toString();
859     }
860 
861     /**
862      * Pretty prints the XML node.
863      * 
864      * @param node xml node to print
865      * 
866      * @return pretty-printed xml
867      */
868     public static String prettyPrintXML(Node node) {
869         StringWriter writer = new StringWriter();
870         writeNode(node, writer,  getPrettyPrintParams());
871         return writer.toString();
872     }
873     
874     /**
875      * Create the parameters set used in pretty print formatting of an LSSerializer.
876      * 
877      * @return the params map
878      */
879     private static Map<String, Object> getPrettyPrintParams() {
880         if (prettyPrintParams == null) {
881             prettyPrintParams = new LazyMap<String, Object>();
882             prettyPrintParams.put("format-pretty-print", Boolean.TRUE);
883         }
884         return prettyPrintParams;
885     }
886 
887     /**
888      * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The written content is encoded using
889      * the encoding specified in the writer configuration.
890      * 
891      * @param node the node to write out
892      * @param output the writer to write the XML to
893      */
894     public static void writeNode(Node node, Writer output) {
895         writeNode(node, output, null);
896     }
897     
898     /**
899      * Writes a Node out to a Writer using the DOM, level 3, Load/Save serializer. The written content is encoded using
900      * the encoding specified in the writer configuration.
901      * 
902      * @param node the node to write out
903      * @param output the writer to write the XML to
904      * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
905      *         instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
906      */
907     public static void writeNode(Node node, Writer output, Map<String, Object> serializerParams) {
908         DOMImplementationLS domImplLS = getLSDOMImpl(node);
909         
910         LSSerializer serializer = getLSSerializer(domImplLS, serializerParams);
911 
912         LSOutput serializerOut = domImplLS.createLSOutput();
913         serializerOut.setCharacterStream(output);
914 
915         serializer.write(node, serializerOut);
916     }
917     
918     /**
919      * Writes a Node out to an OutputStream using the DOM, level 3, Load/Save serializer. The written content
920      * is encoded using the encoding specified in the output stream configuration.
921      * 
922      * @param node the node to write out
923      * @param output the output stream to write the XML to
924      */
925     public static void writeNode(Node node, OutputStream output) {
926         writeNode(node, output, null);
927     }
928 
929 
930     /**
931      * Writes a Node out to an OutputStream using the DOM, level 3, Load/Save serializer. The written content 
932      * is encoded using the encoding specified in the output stream configuration.
933      * 
934      * @param node the node to write out
935      * @param output the output stream to write the XML to
936      * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
937      *         instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
938      */
939     public static void writeNode(Node node, OutputStream output, Map<String, Object> serializerParams) {
940         DOMImplementationLS domImplLS = getLSDOMImpl(node);
941         
942         LSSerializer serializer = getLSSerializer(domImplLS, serializerParams);
943 
944         LSOutput serializerOut = domImplLS.createLSOutput();
945         serializerOut.setByteStream(output);
946 
947         serializer.write(node, serializerOut);
948     }
949     
950     /**
951      * Obtain a the DOM, level 3, Load/Save serializer {@link LSSerializer} instance from the
952      * given {@link DOMImplementationLS} instance.
953      * 
954      * <p>
955      * The serializer instance will be configured with the parameters passed as the <code>serializerParams</code>
956      * argument. It will also be configured with an {@link LSSerializerFilter} that shows all nodes to the filter, 
957      * and accepts all nodes shown.
958      * </p>
959      * 
960      * @param domImplLS the DOM Level 3 Load/Save implementation to use
961      * @param serializerParams parameters to pass to the {@link DOMConfiguration} of the serializer
962      *         instance, obtained via {@link LSSerializer#getDomConfig()}. May be null.
963      *         
964      * @return a new LSSerializer instance
965      */
966     public static LSSerializer getLSSerializer(DOMImplementationLS domImplLS, Map<String, Object> serializerParams) {
967         LSSerializer serializer = domImplLS.createLSSerializer();
968         
969         serializer.setFilter(new LSSerializerFilter() {
970 
971             public short acceptNode(Node arg0) {
972                 return FILTER_ACCEPT;
973             }
974 
975             public int getWhatToShow() {
976                 return SHOW_ALL;
977             }
978         });
979         
980         
981         if (serializerParams != null) {
982             DOMConfiguration serializerDOMConfig = serializer.getDomConfig();
983             for (String key : serializerParams.keySet()) {
984                 serializerDOMConfig.setParameter(key, serializerParams.get(key));
985             }
986         }
987         
988         return serializer;
989     }
990     
991     /**
992      * Get the DOM Level 3 Load/Save {@link DOMImplementationLS} for the given node.
993      * 
994      * @param node the node to evaluate
995      * @return the DOMImplementationLS for the given node
996      */
997     public static DOMImplementationLS getLSDOMImpl(Node node) {
998         DOMImplementation domImpl;
999         if (node instanceof Document) {
1000             domImpl = ((Document) node).getImplementation();
1001         } else {
1002             domImpl = node.getOwnerDocument().getImplementation();
1003         }
1004 
1005         DOMImplementationLS domImplLS = (DOMImplementationLS) domImpl.getFeature("LS", "3.0");
1006         return domImplLS;
1007     }
1008 
1009     /**
1010      * Converts a QName into a string that can be used for attribute values or element content.
1011      * 
1012      * @param qname the QName to convert to a string
1013      * 
1014      * @return the string value of the QName
1015      */
1016     public static String qnameToContentString(QName qname) {
1017         StringBuffer buf = new StringBuffer();
1018 
1019         if (qname.getPrefix() != null) {
1020             buf.append(qname.getPrefix());
1021             buf.append(":");
1022         }
1023         buf.append(qname.getLocalPart());
1024         return buf.toString();
1025     }
1026 
1027     /**
1028      * Ensures that all the visibly used namespaces referenced by the given Element or its descendants are declared by
1029      * the given Element or one of its descendants.
1030      * 
1031      * <strong>NOTE:</strong> This is a very costly operation.
1032      * 
1033      * @param domElement the element to act as the root of the namespace declarations
1034      * 
1035      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
1036      */
1037     public static void rootNamespaces(Element domElement) throws XMLParserException {
1038         rootNamespaces(domElement, domElement);
1039     }
1040 
1041     /**
1042      * Recursively called function that ensures all the visibly used namespaces referenced by the given Element or its
1043      * descendants are declared if they don't appear in the list of already resolved namespaces.
1044      * 
1045      * @param domElement the Element
1046      * @param upperNamespaceSearchBound the "root" element of the fragment where namespaces may be rooted
1047      * 
1048      * @throws XMLParserException thrown if a namespace prefix is encountered that can't be resolved to a namespace URI
1049      */
1050     private static void rootNamespaces(Element domElement, Element upperNamespaceSearchBound) throws XMLParserException {
1051         String namespaceURI = null;
1052         String namespacePrefix = domElement.getPrefix();
1053 
1054         // Check if the namespace for this element is already declared on this element
1055         boolean nsDeclaredOnElement = false;
1056         if (namespacePrefix == null) {
1057             nsDeclaredOnElement = domElement.hasAttributeNS(null, XMLConstants.XMLNS_PREFIX);
1058         } else {
1059             nsDeclaredOnElement = domElement.hasAttributeNS(XMLConstants.XMLNS_NS, namespacePrefix);
1060         }
1061 
1062         if (!nsDeclaredOnElement) {
1063             // Namspace for element was not declared on the element itself, see if the namespace is declared on
1064             // an ancestral element within the subtree where namespaces much be rooted
1065             namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
1066 
1067             if (namespaceURI == null) {
1068                 // Namespace for the element is not declared on any ancestral nodes within the subtree where namespaces
1069                 // must be rooted. Resolve the namespace from ancestors outside that subtree.
1070                 namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
1071                 if (namespaceURI != null) {
1072                     // Namespace resolved outside the subtree where namespaces must be declared so declare the namespace
1073                     // on this element (within the subtree).
1074                     appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
1075                 } else {
1076                     // Namespace couldn't be resolved from any ancestor. If the namespace prefix is null then the
1077                     // element is simply in the undeclared default document namespace, which is fine. If it isn't null
1078                     // then a namespace prefix, that hasn't properly been declared, is being used.
1079                     if (namespacePrefix != null) {
1080                         throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
1081                                 + " found on element " + getNodeQName(domElement));
1082                     }
1083                 }
1084             }
1085         }
1086 
1087         // Make sure all the attribute URIs are rooted here or have been rooted in an ancestor
1088         NamedNodeMap attributes = domElement.getAttributes();
1089         Node attributeNode;
1090         for (int i = 0; i < attributes.getLength(); i++) {
1091             namespacePrefix = null;
1092             namespaceURI = null;
1093             attributeNode = attributes.item(i);
1094 
1095             // Shouldn't need this check, but just to be safe, we have it
1096             if (attributeNode.getNodeType() != Node.ATTRIBUTE_NODE) {
1097                 continue;
1098             }
1099 
1100             namespacePrefix = attributeNode.getPrefix();
1101             if (!DatatypeHelper.isEmpty(namespacePrefix)) {
1102                 // If it's the "xmlns" prefix then it is the namespace declaration,
1103                 // don't try to look it up and redeclare it
1104                 if (namespacePrefix.equals(XMLConstants.XMLNS_PREFIX)
1105                         || namespacePrefix.equals(XMLConstants.XML_PREFIX)) {
1106                     continue;
1107                 }
1108 
1109                 // check to see if the namespace for the prefix has already been defined within the XML fragment
1110                 namespaceURI = lookupNamespaceURI(domElement, upperNamespaceSearchBound, namespacePrefix);
1111                 if (namespaceURI == null) {
1112                     namespaceURI = lookupNamespaceURI(upperNamespaceSearchBound, null, namespacePrefix);
1113                     if (namespaceURI == null) {
1114                         throw new XMLParserException("Unable to resolve namespace prefix " + namespacePrefix
1115                                 + " found on attribute " + getNodeQName(attributeNode) + " found on element "
1116                                 + getNodeQName(domElement));
1117                     }
1118 
1119                     appendNamespaceDeclaration(domElement, namespaceURI, namespacePrefix);
1120                 }
1121             }
1122         }
1123 
1124         // Now for the child elements, we pass a copy of the resolved namespace list in order to
1125         // maintain proper scoping of namespaces.
1126         NodeList childNodes = domElement.getChildNodes();
1127         Node childNode;
1128         for (int i = 0; i < childNodes.getLength(); i++) {
1129             childNode = childNodes.item(i);
1130             if (childNode.getNodeType() == Node.ELEMENT_NODE) {
1131                 rootNamespaces((Element) childNode, upperNamespaceSearchBound);
1132             }
1133         }
1134     }
1135 
1136     /**
1137      * Shortcut for checking a DOM element node's namespace and local name.
1138      * 
1139      * @param e An element to compare against
1140      * @param ns An XML namespace to compare
1141      * @param localName A local name to compare
1142      * @return true iff the element's local name and namespace match the parameters
1143      */
1144     public static boolean isElementNamed(Element e, String ns, String localName) {
1145         return e != null && DatatypeHelper.safeEquals(ns, e.getNamespaceURI())
1146                 && DatatypeHelper.safeEquals(localName, e.getLocalName());
1147     }
1148 
1149     /**
1150      * Gets the first child Element of the node, skipping any Text nodes such as whitespace.
1151      * 
1152      * @param n The parent in which to search for children
1153      * @return The first child Element of n, or null if none
1154      */
1155     public static Element getFirstChildElement(Node n) {
1156         Node child = n.getFirstChild();
1157         while (child != null && child.getNodeType() != Node.ELEMENT_NODE) {
1158             child = child.getNextSibling();
1159         }
1160 
1161         if (child != null) {
1162             return (Element) child;
1163         } else {
1164             return null;
1165         }
1166     }
1167 
1168     /**
1169      * Gets the next sibling Element of the node, skipping any Text nodes such as whitespace.
1170      * 
1171      * @param n The sibling to start with
1172      * @return The next sibling Element of n, or null if none
1173      */
1174     public static Element getNextSiblingElement(Node n) {
1175         Node sib = n.getNextSibling();
1176         while (sib != null && sib.getNodeType() != Node.ELEMENT_NODE) {
1177             sib = sib.getNextSibling();
1178         }
1179 
1180         if (sib != null) {
1181             return (Element) sib;
1182         } else {
1183             return null;
1184         }
1185     }
1186 
1187     /**
1188      * Converts a lexical duration, as defined by XML Schema 1.0, into milliseconds.
1189      * 
1190      * @param duration lexical duration representation
1191      * 
1192      * @return duration in milliseconds
1193      */
1194     public static long durationToLong(String duration) {
1195         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1196         return xmlDuration.getTimeInMillis(new GregorianCalendar());
1197     }
1198 
1199     /**
1200      * Converts a duration in milliseconds to a lexical duration, as defined by XML Schema 1.0.
1201      * 
1202      * @param duration the duration
1203      * 
1204      * @return the lexical representation
1205      */
1206     public static String longToDuration(long duration) {
1207         Duration xmlDuration = getDataTypeFactory().newDuration(duration);
1208         return xmlDuration.toString();
1209     }
1210 
1211 }