View Javadoc

1   /*
2    * The MIT License
3    *
4    * Original work sponsored and donated by National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
5    *
6    * Copyright (C) 2011 National Board of e-Health (NSI), Denmark (http://www.nsi.dk)
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy of
9    * this software and associated documentation files (the "Software"), to deal in
10   * the Software without restriction, including without limitation the rights to
11   * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12   * of the Software, and to permit persons to whom the Software is furnished to do
13   * so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in all
16   * copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24   * SOFTWARE.
25   *
26   * $HeadURL: https://svn.softwareborsen.dk/sosi/trunk/modules/seal/src/main/java/dk/sosi/seal/xml/XmlUtil.java $
27   * $Id: XmlUtil.java 9530 2011-12-15 12:19:33Z chg@lakeside.dk $
28   */
29  package dk.sosi.seal.xml;
30  
31  import dk.sosi.seal.SOSIFactory;
32  import dk.sosi.seal.model.ModelException;
33  import dk.sosi.seal.pki.AuditEventHandler;
34  import org.apache.commons.codec.binary.Base64;
35  import org.apache.xml.security.utils.IdResolver;
36  import org.apache.xml.utils.PrefixResolver;
37  import org.apache.xpath.XPathAPI;
38  import org.w3c.dom.*;
39  import org.xml.sax.InputSource;
40  import org.xml.sax.SAXException;
41  
42  import javax.xml.parsers.DocumentBuilder;
43  import javax.xml.parsers.DocumentBuilderFactory;
44  import javax.xml.parsers.ParserConfigurationException;
45  import javax.xml.transform.*;
46  import javax.xml.transform.dom.DOMSource;
47  import javax.xml.transform.stream.StreamResult;
48  import java.io.*;
49  import java.security.MessageDigest;
50  import java.security.NoSuchAlgorithmException;
51  import java.security.SecureRandom;
52  import java.security.cert.X509Certificate;
53  import java.text.DateFormat;
54  import java.text.ParseException;
55  import java.text.SimpleDateFormat;
56  import java.util.*;
57  
58  /**
59   * XML utility functions. <p/>
60   *
61   * @author kkj
62   * @author $LastChangedBy: chg@lakeside.dk $
63   * @since 1.0
64   */
65  public class XmlUtil {
66  
67  	public static final String XML_ENCODING = "UTF-8";
68  
69  	public static String SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
70  	public static String XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
71  	public static String SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
72  
73  	private static final char[] HEXCHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
74  
75  	private static final int DEFAULT_INDENT = 2;
76  
77  	/**
78  	 * Schema full checking feature id
79  	 * (http://apache.org/xml/features/validation/schema-full-checking).
80  	 */
81  	protected static final String SCHEMA_FULL_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking";
82  
83  	/**
84  	 * Honour all schema locations feature id
85  	 * (http://apache.org/xml/features/honour-all-schemaLocations).
86  	 */
87  	protected static final String HONOUR_ALL_SCHEMA_LOCATIONS_ID = "http://apache.org/xml/features/honour-all-schemaLocations";
88  
89  	/**
90  	 * Validate schema annotations feature id
91  	 * (http://apache.org/xml/features/validate-annotations)
92  	 */
93  	protected static final String VALIDATE_ANNOTATIONS_ID = "http://apache.org/xml/features/validate-annotations";
94  
95  	/**
96  	 * Generate synthetic schema annotations feature id
97  	 * (http://apache.org/xml/features/generate-synthetic-annotations).
98  	 */
99  	protected static final String GENERATE_SYNTHETIC_ANNOTATIONS_ID = "http://apache.org/xml/features/generate-synthetic-annotations";
100 
101 	static final DocumentBuilderFactory CACHED_DOCUMENT_BUILDER_FACTORY;
102 
103 	private static Document EMPTY_DOCUMENT;
104 
105 	private static ClasspathResourceResolver resourceResolver = new ClasspathResourceResolver();
106 
107 	static {
108 		// Initialize and cache a DocumentBuilderFactory and an empty document at class load
109 		CACHED_DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
110 		try {
111 			EMPTY_DOCUMENT = CACHED_DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().newDocument();
112 		} catch (ParserConfigurationException e) {
113 			throw new XmlUtilException("Unable to initialize cached empty document", e);
114 		}
115 	}
116 
117 	// default settings
118 
119 	/**
120 	 * Note: SimpleDateFormat is not thread safe, hence we can't have it as a
121 	 * static local.
122 	 * @param useZuluTime
123 	 * 		whether the formatter should convert dates to UTC and represent them as such
124 	 * @return Dateformatter, following DGWS dateTime format.
125 	 */
126 	public static DateFormat getDateFormat(boolean useZuluTime) {
127 		String formatString = "yyyy'-'MM'-'dd'T'HH:mm:ss";
128 		if (useZuluTime) {
129 			SimpleDateFormat simpleDateFormat = new SimpleDateFormat(formatString + "'Z'");
130 			simpleDateFormat.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
131 			return simpleDateFormat;
132 		} else {
133 			return new SimpleDateFormat(formatString);
134 		}
135 	}
136 
137 	/**
138 	 * Create a new document builder from the cached factory
139 	 *
140 	 * @param validate if <code>true</code> the resulting <code>DocumentBuilder</code> will validate against XML Schema
141 	 * @param useCachedFactory  if <code>true</code> the <code>DocumentBuilder</code> will be drawn from the cached <code>DocumentBuilderFactory</code>.
142 	 *
143 	 * @return A document builder
144 	 */
145 	protected static DocumentBuilder getDocumentBuilder(boolean validate, boolean useEnhancedValidation, boolean useCachedFactory, String rootSchema) {
146 
147 		DocumentBuilder documentBuilder = null;
148 
149 		try {
150 			// DOMSource
151 			DocumentBuilderFactory docBuilderFactory = null;
152 			if(!useCachedFactory) {
153 				docBuilderFactory = DocumentBuilderFactory.newInstance();
154 			} else {
155 				docBuilderFactory = CACHED_DOCUMENT_BUILDER_FACTORY;
156 			}
157 
158 			synchronized(docBuilderFactory) {
159                 // If the docBuilderFactory is a thread-common resource, the usage must be synchronized otherwise it is not thread-safe.
160                 // By synchronizing on the docBuilderFactory object this will only result in a lock, when using  the cached factory.
161                 if(validate) {
162                     docBuilderFactory.setAttribute(SCHEMA_LANGUAGE, XML_SCHEMA);
163                     InputStream schemaStream = resourceResolver.getResourceAsStream("/" + rootSchema);
164                     docBuilderFactory.setAttribute(SCHEMA_SOURCE, schemaStream);
165                 }
166 
167                 docBuilderFactory.setNamespaceAware(true);
168                 docBuilderFactory.setValidating(validate);
169 
170 				documentBuilder = docBuilderFactory.newDocumentBuilder();
171 				documentBuilder.setEntityResolver(resourceResolver );
172 			}
173 		} catch (ParserConfigurationException e) {
174 			throw new XmlUtilException("Unable to initialize XML parser", e);
175 		} catch (IOException e) {
176 			throw new XmlUtilException("Unable to initialize XML parser", e);
177 		}
178 
179 		documentBuilder.setErrorHandler(new DebugErrorHandler(false));
180 		return documentBuilder;
181 	}
182 
183     /**
184      * crete a pretty string representation of this xml string
185      */
186     public static String getPrettyString(String xml) {
187         Document doc = readXml(new Properties(), xml, false);
188         return node2String(doc.getDocumentElement(), true, true);
189     }
190 
191 	/**
192 	 * Convert an XML representation of a String to a DOM Document
193 	 *
194 	 * @param xml  XML String
195 	 * @param properties  A Property Set containing information about auditlogging classes etc.
196 	 * @param validate If <code>true</code> the document will be XMLSchema validated while parsed
197 	 *
198 	 * @return Document representing the XML
199 	 */
200 	public static Document readXml(Properties properties, String xml, boolean validate) throws XmlUtilException {
201 		return readXml(properties, new InputSource(new StringReader(xml)), validate);
202 	}
203 
204 	public static Element getElementByIdAndTagNameNS(String tag, String namespace, String id, Document document) {
205 	    
206 	    NodeList nodes;
207 	    if(namespace == null) {
208 	        nodes = document.getElementsByTagName(tag);
209 	    } else {
210 	        nodes = document.getElementsByTagNameNS(namespace, tag);
211 	    }
212 	    
213 		if (nodes.getLength() == 0) {
214 			return null; // NOPMD
215 		}
216 
217 		for (int i = 0; i < nodes.getLength(); i++) {
218 
219 			Node node = nodes.item(i);
220 			NamedNodeMap attributes = node.getAttributes();
221 
222 			for (int j = 0; j < attributes.getLength(); j++) {
223 
224 				Node attribute = attributes.item(j);
225 				String name = attribute.getNodeName().toLowerCase();
226 
227 				if (name.equals("wsu:id") || (name.equals("id"))) {
228 					String attributeValue = attribute.getNodeValue();
229 					if (id.equalsIgnoreCase(attributeValue)) {
230 						return (Element) node; // NOPMD
231 					}
232 				}
233 			}
234 
235 		}
236 		return null;
237 	}
238 
239 	/**
240 	 * Read an xml input source and optionally validate it against the schema.
241 	 * Only schema validation will be performed (ie. no xml signature checking
242 	 * done here).
243 	 *
244 	 * @param isXml
245 	 *            The input source with the XML in it
246 	 * @param validate
247 	 *            True, if local schema validation is turned on
248 	 * @return The Document
249 	 * @throws XmlUtilException
250 	 *             If parsing failed, validation failed
251 	 */
252 	public static Document readXml(Properties properties, InputSource isXml, boolean validate) throws XmlUtilException {
253 
254 		boolean useDocumentFactoryCache = properties.getProperty(SOSIFactory.PROPERTYNAME_SOSI_USE_DOCUMENT_BUILDER_FACTORY_CACHE, SOSIFactory.PROPERTYVALUE_SOSI_USE_DOCUMENT_BUILDER_FACTORY_CACHE).equalsIgnoreCase("true");
255 		boolean useEnhancedValidation = properties.getProperty(SOSIFactory.PROPERTYNAME_SOSI_VALIDATE_ENHANCED, SOSIFactory.PROPERTYVALUE_SOSI_VALIDATE_ENHANCED).equalsIgnoreCase("true");
256 
257         String defaultSchema = "soap.xsd";
258         if(useEnhancedValidation) defaultSchema = "soap-specialized.xsd";
259 
260         String rootSchema = properties.getProperty(SOSIFactory.PROPERTYNAME_SOSI_ROOTSCHEMA, defaultSchema);
261 
262 		DocumentBuilder documentBuilder = getDocumentBuilder(validate, useEnhancedValidation, useDocumentFactoryCache, rootSchema);
263 
264 		Document doc = null;
265 		try {
266 			doc = documentBuilder.parse(isXml);
267 		} catch (SAXException e) {
268 			SOSIFactory.getAuditEventHandler(properties).onInformationalAuditingEvent(
269 					AuditEventHandler.EVENT_TYPE_ERROR_PARSING_SOSI_XML,
270 					new Object[]{doc}
271 					);
272 			throw new XmlUtilException("Unable to parse XML", e);
273 		} catch (IOException e) {
274 			SOSIFactory.getAuditEventHandler(properties).onInformationalAuditingEvent(
275 					AuditEventHandler.EVENT_TYPE_ERROR_PARSING_SOSI_XML,
276 					new Object[]{doc}
277 					);
278 			throw new XmlUtilException("Unable to parse XML", e);
279 		}
280 
281 		SOSIFactory.getAuditEventHandler(properties).onInformationalAuditingEvent(
282 				AuditEventHandler.EVENT_TYPE_INFO_SOSI_XML_VALIDATED,
283 				new Object[]{doc}
284 				);
285 
286 		return doc;
287 
288 	}
289 
290 	/**
291 	 * Convert the supplied set of bytes to base64 encoding
292 	 *
293 	 * @param bytes
294 	 *            Bytes to convert
295 	 * @return Base64 representation of bytes
296 	 */
297 	public static String toBase64(byte[] bytes) {
298 		return Base64.encodeBase64String(bytes);
299 	}
300 
301 	/**
302 	 * Convert the supplied base 64 encoded string to raw bytes
303 	 *
304 	 * @param data
305 	 *            Base 64 encoded string
306 	 * @return raw byte representation
307 	 */
308 	public static byte[] fromBase64(String data) {
309 	    return Base64.decodeBase64(data);
310 	}
311 
312 	/**
313 	 * Create empty document.
314 	 */
315 	public static Document createEmptyDocument() {
316 		synchronized(EMPTY_DOCUMENT) {
317 			return (Document) EMPTY_DOCUMENT.cloneNode(false);
318 		}
319 	}
320 
321 	/**
322 	 * Convert the supplied byte array to a hex string
323 	 *
324 	 * @param bytes
325 	 *            to convert
326 	 * @return A hexadecimal string representation of the supplied bytes
327 	 */
328 	public static String toHex(byte[] bytes) {
329 
330 		int i = 0;
331 		StringBuffer stringBuffer = new StringBuffer();
332 		while (i < bytes.length) {
333 			byte curByte = bytes[i++];
334 			stringBuffer.append(HEXCHARS[(curByte & 0xF0) >> 4]).append(HEXCHARS[curByte & 0x0F]);
335 		}
336 		return stringBuffer.toString();
337 	}
338 
339 	/**
340 	 * given a <node>text</node>, return the value of text textnode embedded
341 	 * inside.
342 	 *
343 	 * @param parent
344 	 *            The node that has a textnode child element
345 	 * @return The node value of the text node
346 	 * @throws XmlUtilException
347 	 *             If the node is not a textnode.
348 	 */
349 	public static String getTextNodeValue(Node parent) throws XmlUtilException {
350 
351 		NodeList children = parent.getChildNodes();
352 		if (children.getLength() == 0)
353 			throw new XmlUtilException("The supplied element doesn't have child nodes");
354 
355 		Node child = children.item(0);
356 
357 		if (child.getNodeType() != Node.TEXT_NODE && child.getNodeType() != Node.CDATA_SECTION_NODE)
358 			throw new XmlUtilException("The first child of the supplied node is not a text element");
359 
360 		return child.getNodeValue();
361 	}
362 
363     public static Element getFirstChildElementNS(Element parent, String namespaceURI, String localName) {
364         final NodeList childNodes = parent.getChildNodes();
365 
366         for (int i = 0; i < childNodes.getLength(); i++) {
367             final Node childNode = childNodes.item(i);
368             if (childNode instanceof Element) {
369                 Element childElement = (Element) childNode;
370                 if (localName.equals(childElement.getLocalName()) && namespaceURI.equals(childElement.getNamespaceURI())) {
371                     return childElement;
372                 }
373             }
374         }
375 
376         return null;
377     }
378 
379     public static List<Element> getChildElementsNS(Element parent, String namespaceURI, String localName) {
380         final NodeList childNodes = parent.getChildNodes();
381 
382         List<Element> elements = new LinkedList<Element>();
383 
384         for (int i = 0; i < childNodes.getLength(); i++) {
385             final Node childNode = childNodes.item(i);
386             if (childNode instanceof Element) {
387                 Element childElement = (Element) childNode;
388                 if (localName.equals(childElement.getLocalName()) && namespaceURI.equals(childElement.getNamespaceURI())) {
389                     elements.add(childElement);
390                 }
391             }
392         }
393 
394         return elements;
395     }
396 
397     /**
398 	 * Serialize the supplied DOM node to string including an <code><?xml ...></code> header and without prettyprinting.
399 	 *
400 	 * Calls node2String(node, false, true);
401 	 *
402 	 * @param node
403 	 *            The node to serialize
404 	 * @return DOM as XML String
405 	 */
406 	public static String node2String(Node node) {
407 		return node2String(node, false, true);
408 	}
409 
410 	/**
411 	 * Serialize the supplied DOM node to string form. This result is configurable through parameterettings.
412 	 * <p/>
413 	 * <b>WARNING: </b>If the DOM document contains digital signatures (more precisely <code><SignedInfo></code> elements the signatures
414 	 * will be broken if the XML document is formatted through pretty-printing!
415 	 *
416 	 * @param node
417 	 *            The node to serialize
418 	 * @param pretty
419 	 *            If true, will indent and generally pretty-print XML. Note:
420 	 *            This may affect validity of  contained digital signatures!
421 	 * @param includeXMLHeader
422 	 *            If true, add the standard XML header to the output
423 	 * @return DOM as XML String
424 	 */
425 	public static String node2String(Node node, boolean pretty, boolean includeXMLHeader) {
426 
427 		ByteArrayOutputStream bas = new ByteArrayOutputStream();
428 		try {
429 			TransformerFactory factory = TransformerFactory.newInstance();
430 			Transformer transformer = factory.newTransformer();
431 
432 			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
433 			transformer.setOutputProperty(OutputKeys.METHOD, "xml");
434 			transformer.setOutputProperty(OutputKeys.INDENT, (pretty) ? "yes" : "no");
435 			transformer.setOutputProperty(OutputKeys.ENCODING, XML_ENCODING);
436 			transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", Integer.toString(DEFAULT_INDENT));
437 
438 			transformer.transform(new DOMSource(node), new StreamResult(bas));
439 
440 			String str = bas.toString(XML_ENCODING);
441 			if(includeXMLHeader) {
442 				str = "<?xml version=\"1.0\" encoding=\""+XML_ENCODING+"\" ?>"+((pretty)?"\n"+str:str);
443 			}
444 			return str;
445 		} catch (TransformerConfigurationException e) {
446 			throw new XmlUtilException("TransformerConfigurationException during prettyPrint", e);
447 		} catch (TransformerException e) {
448 			throw new XmlUtilException("TransformerException during prettyPrint", e);
449 		} catch (UnsupportedEncodingException e) {
450 			throw new XmlUtilException("Unsupported XML encoding", e);
451 		}
452 	}
453 
454 	public static byte[] serializeXml2ByteArray(Node node, boolean includeXMLHeader) {
455 
456 		// output the resulting document
457 		ByteArrayOutputStream os = new ByteArrayOutputStream();
458 		TransformerFactory tf = TransformerFactory.newInstance();
459 		try {
460 			Transformer trans = tf.newTransformer();
461 			trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, (includeXMLHeader) ? "no" : "yes");
462 			trans.setOutputProperty(OutputKeys.METHOD, "xml");
463 			trans.transform(new DOMSource(node), new StreamResult(os));
464 		} catch (TransformerException e) {
465 			throw new XmlUtilException("Unable to gctransform input", e);
466 		}
467 		return os.toByteArray();
468 	}
469 
470 	/**
471 	 * Find the first child of the parent element of the specified nodeType.
472 	 * Note: This method does a recursive scan of all elements in the subtree
473 	 * until at match is found. Use with caution.
474 	 *
475 	 * @param parent
476 	 * @param nodeType
477 	 * @return The first child element of the specified nodeType
478 	 */
479 	public static Node findChildElement(Node parent, String nodeType) {
480 
481 		NodeList children = parent.getChildNodes();
482 		if (children.getLength() == 0)
483 			return null; // NOPMD
484 
485 		for (int i = 0; i < children.getLength(); i++) {
486 
487 			Node child = children.item(i);
488 			if (child.getNodeName().equals(nodeType))
489 				return child; // NOPMD
490 			child = findChildElement(child, nodeType);
491 			if (child != null)
492 				return child; // NOPMD
493 		}
494 
495 		return null;
496 	}
497 
498 	/**
499 	 * Convert a byte array to an X509 certificate
500      *
501      * Use the method in CertificateParser instead
502 	 *
503 	 * @param value
504 	 *            The byte array to convert
505 	 * @return The X509 certificate
506 	 * @throws ModelException
507 	 *             if conversion fails
508 	 */
509     @Deprecated
510 	public static X509Certificate getByteArrayAsCertificate(byte[] value) {
511         return CertificateParser.asCertificate(value);
512 	}
513 
514 	/**
515 	 * See http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#dateTime
516 	 * @param useZuluTime
517 	 * 			whether to convert the date to UTC and represent it accordingly
518 	 */
519 	public static String toXMLTimeStamp(Date date, boolean useZuluTime) {
520 
521 		return getDateFormat(useZuluTime).format(date);
522 	}
523 
524 	/**
525 	 * See http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#dateTime
526 	 */
527 	public static Date fromXMLTimeStamp(String xmlTimestamp) throws ParseException {
528 
529 		if (xmlTimestamp == null)
530 			throw new ModelException("xmlTimestamp cannot be null");
531 		boolean useZuluTime = isZuluTimeFormat(xmlTimestamp);
532 		return getDateFormat(useZuluTime).parse(xmlTimestamp);
533 	}
534 
535 	public static boolean isZuluTimeFormat(String xmlTimestamp) {
536 		return xmlTimestamp != null && xmlTimestamp.endsWith("Z");
537 	}
538 
539 	// this moves the timeconsuming instantiation of SecureRandom
540 	// from first messagegeneration to class init
541 	static {
542 		createGUID();
543 	}
544 
545 	//TODO use UUID algorithm based on standards (JDK1.5, Commons Id contains such a generator)
546 	public static String createGUID() {
547 	    return toBase64(createUIDBytes(16));
548 	}
549 
550     public static String generateUUID() {
551         return "urn:uuid:" + UUID.randomUUID();
552     }
553 
554     public static String generateRandomNCName() {
555         return "_" + UUID.randomUUID();
556     }
557 
558 	public static String createNonce() {
559 
560 		long now = System.currentTimeMillis();
561 		byte[] nonce = new byte[20];
562 
563 		// Copy currentTimeMillis to the upper 8 bytes
564 		for (int i = 0; i < 8; i++) {
565 			nonce[7 - i] = (byte) (now & 0xFF);
566 			now = now >>> 8;
567 		}
568 
569 		// Copy 8 random bytes to the lower 8 bytes
570 		System.arraycopy(createUIDBytes(8), 0, nonce, 8, 8);
571 
572 		// Place a "magic" in the upper 4 bytes
573 		nonce[16] = 0x53;
574 		nonce[17] = 0x4F;
575 		nonce[18] = 0x53;
576 		nonce[19] = 0x49;
577 
578 		return toBase64(nonce);
579 	}
580 
581 	/**
582 	 * Create a SHA-1 hash of the supplied bytes
583 	 *
584 	 * @param bytes
585 	 *            The bytes to create a hash for
586 	 * @return The SHA-1 message digest of the supplied bytes
587 	 */
588 	public static byte[] getSha1Digest(byte[] bytes) {
589 
590 		MessageDigest messageDigest;
591 		try {
592 			messageDigest = MessageDigest.getInstance("SHA-1");
593 		} catch (NoSuchAlgorithmException e) {
594 			throw new ModelException("Unable to get SHA-1 algorithm for message digest", e);
595 		}
596 		return messageDigest.digest(bytes);
597 	}
598 
599 	/**
600 	 * Deep and thorough search for any attribute that lowercased ends with "id"
601 	 * and whose name value matches the reference uri. Potentially very
602 	 * expensive operation.
603 	 *
604 	 * @param root
605 	 *            The start of the search
606 	 * @param referenceUri
607 	 *            The id value to look for
608 	 * @return Null if not found, the Attribute node otherwise.
609 	 */
610 	public static Node getElementByIdExtended(Node root, String referenceUri) {
611 
612 		NamedNodeMap namedNodeMap = root.getAttributes();
613 		if (namedNodeMap != null) {
614 			for (int i = 0; i < namedNodeMap.getLength(); i++) {
615 
616 				Node node = namedNodeMap.item(i);
617 				String name = node.getNodeName().toLowerCase();
618 				if (name.endsWith("id")) {
619 					String value = node.getNodeValue();
620 					//if (referenceUri.equals(value))
621                     if (value.equals(referenceUri))
622 						return root; // NOPMD
623 				}
624 			}
625 		}
626 
627 		NodeList children = root.getChildNodes();
628 		for (int i = 0; i < children.getLength(); i++) {
629 			Node candidate = getElementByIdExtended(children.item(i), referenceUri);
630 			if (candidate != null)
631 				return candidate; // NOPMD
632 		}
633 
634 		return null;
635 	}
636 
637 	/**
638 	 * Traverse the node hierarchy upwards to the root, adding each attribute
639 	 * that ends with "id" (case ignored) as an Id to the IdResolver for future
640 	 * lookups. This is necessary when validating external documents that use
641 	 * extended ids such as wsu:id, which are not discovered by normal DOM
642 	 * lookup means.
643 	 *
644 	 * @param startElement
645 	 *            The element at which to start the addition
646 	 */
647 	public static void ensureEnvelopedIds(Element startElement) {
648 
649 		registerElementByIdExtended(startElement);
650 
651 		Node childParent = startElement.getParentNode();
652 		while (childParent != null) {
653 			if (childParent.getNodeType() == Node.ELEMENT_NODE) {
654 				registerElementByIdExtended((Element) childParent);
655 			}
656 			childParent = childParent.getParentNode();
657 		}
658 	}
659 
660 	/**
661 	 * Run thru the attributes of the element, adding any attribute that ends
662 	 * with "id" (case ignored) as an Id to the IdResolver.
663 	 *
664 	 * @param element
665 	 *            The element for which to add ids.
666 	 */
667 	public static void registerElementByIdExtended(Element element) {
668 
669 		NamedNodeMap namedNodeMap = element.getAttributes();
670 		if (namedNodeMap != null) {
671 			for (int i = 0; i < namedNodeMap.getLength(); i++) {
672 
673 				Node node = namedNodeMap.item(i);
674 				String name = node.getNodeName().toLowerCase();
675 				if (name.endsWith("id")) {
676 					String value = node.getNodeValue();
677 					IdResolver.registerElementById(element, value);
678 				}
679 			}
680 		}
681 
682 	}
683 
684 	/**
685 	 * Makes a deep compare of two DOM nodes and returns the first subnode that
686 	 * differs in the two DOM representations. The comparisson is significant in
687 	 * respect to order of attributes and children.
688 	 *
689 	 * @param node1
690 	 *            The first node to compare
691 	 * @param node2
692 	 *            The second node to compare
693 	 * @return <code>null</code> if the two Nodes are equal, or the first
694 	 *         subnode that differs.
695 	 */
696 	public static Node deepDiff(Node node1, Node node2) {
697 
698 		Node result = null;
699 		if (!node1.getNodeName().equals(node2.getNodeName())
700 				|| ((node1.getNamespaceURI() == null || node2.getNamespaceURI() == null) && node1.getNamespaceURI() != node2.getNamespaceURI())) {
701 			return node1; // NOPMD
702 		}
703 
704 		// Compare attributes
705 		NamedNodeMap node1List = node1.getAttributes();
706 		NamedNodeMap node2List = node2.getAttributes();
707 		if (node1List != null && node2List != null) {
708 			if (node1List.getLength() != node2List.getLength())
709 				return node1; // NOPMD
710 			for (int i = 0; i < node1List.getLength(); i++) { // size is compared above
711 				if ((result = deepDiff(node1List.item(i), node2List.item(i))) != null) // NOPMD
712 					return result; // NOPMD
713 			}
714 		} else if (node1List != node2List) {
715 			return node1; // NOPMD
716 		}
717 
718 		// Compare children
719 		NodeList children1 = node1.getChildNodes();
720 		NodeList children2 = node2.getChildNodes();
721 		if (children1 != null && children2 != null) {
722 			if (children1.getLength() != children2.getLength())
723 				return node1; // NOPMD
724 			for (int i = 0; i < children1.getLength(); i++) { // size is compared above
725 				if ((result = deepDiff(children1.item(i), children2.item(i))) != null) // NOPMD
726 					return result; // NOPMD
727 			}
728 		} else if (children1 != children2) {
729 			return node1; // NOPMD
730 		}
731 		return null;
732 	}
733 
734 	/**
735 	 * Selects a single Element based on an xpath expression
736 	 *
737 	 * @param node
738 	 * 			The starting Node from which the xpath expression is evaluated
739 	 * @param xpath
740 	 * 			The xpath expression
741 	 * @param resolver
742 	 * 			The PefixResolver used
743 	 * @return
744 	 * 			The element if the expression matches exactly one element or <code>null</null> otherwise.
745 	 */
746 	public static Element selectSingleElement(Node node, String xpath, PrefixResolver resolver) {
747 		try {
748 			NodeList nodeList = XPathAPI.eval(node, xpath, resolver).nodelist();
749 			return (Element) (nodeList.getLength() == 1 ? nodeList.item(0) : null);
750 		} catch (TransformerException e) {
751 			throw new XmlUtilException("Unable to get " + xpath, e);
752 		}
753 	}
754 
755 	public static String removeFormatting(String xml) {
756 		return xml.replaceAll(">\\s*<", "><");
757 	}
758 
759 	// ===================================
760 	// Private parts
761 	// ===================================
762 
763 	private static byte[] createUIDBytes(int size) {
764 
765 		SecureRandom secureRandom = new SecureRandom();
766 		byte[] probablyUniqueID = new byte[size];
767 		secureRandom.nextBytes(probablyUniqueID);
768 		return probablyUniqueID;
769 	}
770 
771 }