1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
60
61
62
63
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
79
80
81 protected static final String SCHEMA_FULL_CHECKING_FEATURE_ID = "http://apache.org/xml/features/validation/schema-full-checking";
82
83
84
85
86
87 protected static final String HONOUR_ALL_SCHEMA_LOCATIONS_ID = "http://apache.org/xml/features/honour-all-schemaLocations";
88
89
90
91
92
93 protected static final String VALIDATE_ANNOTATIONS_ID = "http://apache.org/xml/features/validate-annotations";
94
95
96
97
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
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
118
119
120
121
122
123
124
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
139
140
141
142
143
144
145 protected static DocumentBuilder getDocumentBuilder(boolean validate, boolean useEnhancedValidation, boolean useCachedFactory, String rootSchema) {
146
147 DocumentBuilder documentBuilder = null;
148
149 try {
150
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
160
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
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
193
194
195
196
197
198
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;
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;
231 }
232 }
233 }
234
235 }
236 return null;
237 }
238
239
240
241
242
243
244
245
246
247
248
249
250
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
292
293
294
295
296
297 public static String toBase64(byte[] bytes) {
298 return Base64.encodeBase64String(bytes);
299 }
300
301
302
303
304
305
306
307
308 public static byte[] fromBase64(String data) {
309 return Base64.decodeBase64(data);
310 }
311
312
313
314
315 public static Document createEmptyDocument() {
316 synchronized(EMPTY_DOCUMENT) {
317 return (Document) EMPTY_DOCUMENT.cloneNode(false);
318 }
319 }
320
321
322
323
324
325
326
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
341
342
343
344
345
346
347
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
399
400
401
402
403
404
405
406 public static String node2String(Node node) {
407 return node2String(node, false, true);
408 }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
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
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
472
473
474
475
476
477
478
479 public static Node findChildElement(Node parent, String nodeType) {
480
481 NodeList children = parent.getChildNodes();
482 if (children.getLength() == 0)
483 return null;
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;
490 child = findChildElement(child, nodeType);
491 if (child != null)
492 return child;
493 }
494
495 return null;
496 }
497
498
499
500
501
502
503
504
505
506
507
508
509 @Deprecated
510 public static X509Certificate getByteArrayAsCertificate(byte[] value) {
511 return CertificateParser.asCertificate(value);
512 }
513
514
515
516
517
518
519 public static String toXMLTimeStamp(Date date, boolean useZuluTime) {
520
521 return getDateFormat(useZuluTime).format(date);
522 }
523
524
525
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
540
541 static {
542 createGUID();
543 }
544
545
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
564 for (int i = 0; i < 8; i++) {
565 nonce[7 - i] = (byte) (now & 0xFF);
566 now = now >>> 8;
567 }
568
569
570 System.arraycopy(createUIDBytes(8), 0, nonce, 8, 8);
571
572
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
583
584
585
586
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
601
602
603
604
605
606
607
608
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
621 if (value.equals(referenceUri))
622 return root;
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;
632 }
633
634 return null;
635 }
636
637
638
639
640
641
642
643
644
645
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
662
663
664
665
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
686
687
688
689
690
691
692
693
694
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;
702 }
703
704
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;
710 for (int i = 0; i < node1List.getLength(); i++) {
711 if ((result = deepDiff(node1List.item(i), node2List.item(i))) != null)
712 return result;
713 }
714 } else if (node1List != node2List) {
715 return node1;
716 }
717
718
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;
724 for (int i = 0; i < children1.getLength(); i++) {
725 if ((result = deepDiff(children1.item(i), children2.item(i))) != null)
726 return result;
727 }
728 } else if (children1 != children2) {
729 return node1;
730 }
731 return null;
732 }
733
734
735
736
737
738
739
740
741
742
743
744
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
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 }