1 package org.opentrafficsim.editor;
2
3 import java.io.IOException;
4 import java.net.URI;
5 import java.net.URISyntaxException;
6 import java.util.ArrayList;
7 import java.util.Iterator;
8 import java.util.LinkedHashMap;
9 import java.util.LinkedHashSet;
10 import java.util.LinkedList;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Map.Entry;
14 import java.util.Queue;
15 import java.util.Set;
16
17 import javax.xml.parsers.ParserConfigurationException;
18
19 import org.djutils.exceptions.Throw;
20 import org.w3c.dom.Document;
21 import org.w3c.dom.Node;
22 import org.xml.sax.SAXException;
23
24
25
26
27
28
29
30
31
32
33 public class Schema
34 {
35
36
37 private Node root;
38
39
40 private final Set<String> readFiles = new LinkedHashSet<>();
41
42
43 private final Map<String, Node> types = new LinkedHashMap<>();
44
45
46 private final Map<String, Set<String>> extendedTypes = new LinkedHashMap<>();
47
48
49
50
51
52 private final Map<String, Set<String>> referredTypes = new LinkedHashMap<>();
53
54
55 private final Map<String, Node> elements = new LinkedHashMap<>();
56
57
58 private final Map<String, Set<String>> referredElements = new LinkedHashMap<>();
59
60
61 private final Map<String, String> documentation = new LinkedHashMap<>();
62
63
64 private final Map<String, Node> keys = new LinkedHashMap<>();
65
66
67 private final Map<String, Node> keyrefs = new LinkedHashMap<>();
68
69
70 private final Map<String, Node> uniques = new LinkedHashMap<>();
71
72
73 private final Queue<RecursionElement> queue = new LinkedList<>();
74
75
76 private boolean blockLoop = false;
77
78
79
80
81
82 public Schema(final Document document)
83 {
84 this.readFiles.add(document.getDocumentURI());
85
86 queue("", document, true);
87 while (!this.queue.isEmpty())
88 {
89 RecursionElement next = this.queue.poll();
90 read(next.getPath(), next.getNode(), next.isExtendPath());
91 }
92
93
94 for (Entry<String, Node> entry : this.elements.entrySet())
95 {
96 String referredTypeName = DocumentReader.getAttribute(entry.getValue(), "type");
97 if (referredTypeName != null && !referredTypeName.startsWith("xsd:"))
98 {
99 Node referredType = getType(referredTypeName);
100 entry.setValue(referredType);
101 }
102 }
103
104
105 Set<String> allTypes = new LinkedHashSet<>(this.types.keySet());
106 for (String str : this.extendedTypes.keySet())
107 {
108 allTypes.removeIf((val) -> val.startsWith(str));
109 }
110 for (String str : this.referredTypes.keySet())
111 {
112 allTypes.removeIf((val) -> val.startsWith(str));
113 }
114 if (!allTypes.isEmpty())
115 {
116 System.out.println(allTypes.size() + " types are defined but never extended or referred to.");
117
118 }
119
120 Set<String> allElements = new LinkedHashSet<>(this.elements.keySet());
121 for (String str : this.referredElements.keySet())
122 {
123 allElements.removeIf((val) -> val.startsWith(str));
124 }
125 for (String str : this.types.keySet())
126 {
127 allElements.removeIf((val) -> val.startsWith(str));
128 }
129 allElements.removeIf((path) -> path.startsWith("Ots"));
130 if (!allElements.isEmpty())
131 {
132 System.out.println(allElements.size() + " elements are defined but never referred to, nor are they a type.");
133
134 }
135
136 checkKeys();
137 checkKeyrefs();
138 checkUniques();
139
140
141
142
143
144 System.out.println("Root found as '" + DocumentReader.getAttribute(this.getRoot(), "name") + "'.");
145 System.out.println("Read " + this.readFiles.size() + " files.");
146 System.out.println("Read " + this.elements.size() + " elements.");
147 System.out.println("Read " + this.types.size() + " types.");
148 System.out.println("Read " + this.extendedTypes.size() + " extended types.");
149 System.out.println("Read " + this.documentation.size() + " documentations.");
150 System.out.println("Read " + this.keys.size() + " keys.");
151 System.out.println("Read " + this.keyrefs.size() + " keyrefs.");
152 System.out.println("Read " + this.uniques.size() + " uniques.");
153 for (String type : this.extendedTypes.keySet())
154 {
155 if (!this.types.containsKey(type) && !type.startsWith("xsd:"))
156 {
157 System.err.println("Type '" + type + "' is extended but was not found.");
158 }
159 }
160 }
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187 private void read(final String path, final Node node, final boolean extendPath)
188 {
189 if (recursion(path))
190 {
191 System.out.println("Recursion found at " + path + ", further expansion is halted.");
192 return;
193 }
194
195 if (node.getNodeName().equals("xsd:extension"))
196 {
197 String base = DocumentReader.getAttribute(node, "base").replace("ots:", "");
198 if (!base.startsWith("xsd:"))
199 {
200 Node baseNode = getType(base);
201 if (baseNode == null)
202 {
203 if (this.blockLoop)
204 {
205 return;
206 }
207 this.blockLoop = true;
208
209 queue(path, node, false);
210 }
211 else
212 {
213 queue(path, baseNode, false);
214 }
215 }
216 if (this.extendedTypes.containsKey(base) && this.extendedTypes.get(base).contains(path))
217 {
218
219 return;
220 }
221 this.extendedTypes.computeIfAbsent(base, (key) -> new LinkedHashSet<String>()).add(path);
222 }
223 this.blockLoop = false;
224
225 String nextPath = path;
226 Node nextNode = node;
227 if (node.getNodeName().equals("xsd:element") && node.hasAttributes())
228 {
229
230 String name = DocumentReader.getAttribute(node, "name");
231 if (name != null)
232 {
233 if (name.equals("Ots"))
234 {
235 this.root = node;
236 }
237 nextPath = extendPath ? (nextPath.isEmpty() ? name : nextPath + "." + name) : nextPath;
238 this.elements.put(nextPath, node);
239 }
240 String ref = DocumentReader.getAttribute(node, "ref");
241 if (ref != null)
242 {
243 ref = ref.replace("ots:", "");
244 nextNode = getElement(ref);
245
246
247
248
249
250 if (DocumentReader.getAttribute(nextNode, "type") != null)
251 {
252 element(path, nextNode);
253 }
254 nextPath = extendPath ? (nextPath.isEmpty() ? ref : nextPath + "." + ref) : nextPath;
255 this.elements.put(nextPath, nextNode);
256 Throw.whenNull(nextNode, "Element %s refers to a type that was not loaded in first pass.", nextPath);
257 }
258 }
259
260 String name = DocumentReader.getAttribute(nextNode, "name");
261 String nodeName = nextNode.getNodeName();
262 if (name != null && (nodeName.equals("xsd:complexType") || nodeName.equals("xsd:simpleType")))
263 {
264 this.types.put(name, nextNode);
265 nextPath = extendPath ? (nextPath.isEmpty() ? name : nextPath + "." + name) : nextPath;
266 }
267
268 if (!nextNode.hasChildNodes())
269 {
270 return;
271 }
272 for (int childIndex = 0; childIndex < nextNode.getChildNodes().getLength(); childIndex++)
273 {
274 Node child = nextNode.getChildNodes().item(childIndex);
275 switch (child.getNodeName())
276 {
277 case "#text":
278 break;
279 case "xsd:include":
280 include(nextPath, child);
281 break;
282 case "xsd:element":
283 element(nextPath, child);
284 break;
285 case "xsd:attribute":
286 attribute(nextPath, child);
287 break;
288 case "xsd:documentation":
289 documentation(nextPath, child);
290 break;
291 case "xsd:union":
292 union(nextPath, child);
293 break;
294 case "xsd:key":
295 if (nextPath.startsWith("Ots"))
296 {
297 this.keys.put(nextPath + "." + DocumentReader.getAttribute(child, "name"), child);
298 }
299 break;
300 case "xsd:keyref":
301 if (nextPath.startsWith("Ots"))
302 {
303 this.keyrefs.put(nextPath + "." + DocumentReader.getAttribute(child, "name"), child);
304 }
305 break;
306 case "xsd:unique":
307 if (nextPath.startsWith("Ots"))
308 {
309 this.uniques.put(nextPath + "." + DocumentReader.getAttribute(child, "name"), child);
310 }
311 break;
312 default:
313 read(nextPath, child, true);
314 }
315 }
316 }
317
318
319
320
321
322
323
324 private boolean recursion(final String path)
325 {
326 StringBuffer sub = (new StringBuffer(path)).reverse();
327 int fromIndex = 0;
328 while (fromIndex < sub.length())
329 {
330 int dot = sub.indexOf(".", fromIndex);
331 if (dot < 0)
332 {
333 return false;
334 }
335 int toIndex = 2 * (dot + 1);
336 if (toIndex > sub.length())
337 {
338 return false;
339 }
340 if (sub.substring(0, dot + 1).equals(sub.substring(dot + 1, toIndex)))
341 {
342 return true;
343 }
344 fromIndex = dot + 1;
345 }
346 return false;
347 }
348
349
350
351
352
353
354
355
356
357 private void queue(final String path, final Node node, final boolean extendPath)
358 {
359 this.queue.add(new RecursionElement(path, node, extendPath));
360 }
361
362
363
364
365
366
367 private void include(final String path, final Node node)
368 {
369 String schemaLocation = DocumentReader.getAttribute(node, "schemaLocation");
370 String schemaPath = folder(node) + schemaLocation;
371 if (!this.readFiles.add(schemaPath))
372 {
373 return;
374 }
375 try
376 {
377 read(path, DocumentReader.open(new URI(schemaPath)), true);
378 }
379 catch (SAXException | IOException | ParserConfigurationException | URISyntaxException e)
380 {
381 throw new RuntimeException("Unable to find resource " + folder(node) + schemaLocation);
382 }
383 }
384
385
386
387
388
389
390 private String folder(final Node node)
391 {
392 String uri = node.getBaseURI();
393 if (uri == null)
394 {
395 return "";
396 }
397 int a = uri.lastIndexOf("\\");
398 int b = uri.lastIndexOf("/");
399 return uri.substring(0, (a > b ? a : b) + 1);
400 }
401
402
403
404
405
406
407 private void element(final String path, final Node node)
408 {
409 if (DocumentReader.getAttribute(node, "ref") != null)
410 {
411 ref(path, node);
412 return;
413 }
414 String type = DocumentReader.getAttribute(node, "type");
415 if (type != null && !type.startsWith("xsd:"))
416 {
417 type = type.replace("ots:", "");
418 this.referredTypes.computeIfAbsent(type, (key) -> new LinkedHashSet<>()).add(path.isEmpty()
419 ? DocumentReader.getAttribute(node, "name") : path + "." + DocumentReader.getAttribute(node, "name"));
420 Node referred = getType(type);
421 if (referred == null)
422 {
423 queue(path, node, true);
424 return;
425 }
426 else
427 {
428 queue(path + "." + DocumentReader.getAttribute(node, "name"), referred, false);
429 }
430 }
431 read(path, node, true);
432 }
433
434
435
436
437
438
439
440
441 private void ref(final String path, final Node node)
442 {
443 String ref = DocumentReader.getAttribute(node, "ref").replace("ots:", "");
444 if (ref.equals("xi:include"))
445 {
446 return;
447 }
448 this.referredElements.computeIfAbsent(ref, (key) -> new LinkedHashSet<>()).add(path);
449 Node refNode = getElement(ref);
450 if (refNode == null)
451 {
452 queue(path, node, true);
453 }
454 else
455 {
456 read(path, node, true);
457 }
458 }
459
460
461
462
463
464
465 private void attribute(final String path, final Node node)
466 {
467 if (DocumentReader.getAttribute(node, "type") != null)
468 {
469 String type = DocumentReader.getAttribute(node, "type").replace("ots:", "");
470 String name = DocumentReader.getAttribute(node, "name");
471 this.referredTypes.computeIfAbsent(type, (key) -> new LinkedHashSet<>()).add(path + "." + name);
472 }
473 read(path, node, true);
474 }
475
476
477
478
479
480
481 private void documentation(final String path, final Node node)
482 {
483 this.documentation.put(path, DocumentReader.getChild(node, "#text").getNodeValue().trim().replaceAll("\r\n", " ")
484 .replaceAll("\n", " ").replaceAll("\r", " ").replace(" ", ""));
485 }
486
487
488
489
490
491
492 private void union(final String path, final Node node)
493 {
494 for (String type : DocumentReader.getAttribute(node, "memberTypes").split(" "))
495 {
496 this.referredTypes.computeIfAbsent(type.replace("ots:", ""), (key) -> new LinkedHashSet<>()).add(path);
497 }
498 read(path, node, true);
499 }
500
501
502
503
504
505
506
507
508
509
510 private class RecursionElement
511 {
512
513 private final String path;
514
515
516 private final Node node;
517
518
519 private final boolean extendPath;
520
521
522
523
524
525
526
527 RecursionElement(final String path, final Node node, final boolean extendPath)
528 {
529 this.path = path;
530 this.node = node;
531 this.extendPath = extendPath;
532 }
533
534
535
536
537
538 public String getPath()
539 {
540 return this.path;
541 }
542
543
544
545
546
547 public Node getNode()
548 {
549 return this.node;
550 }
551
552
553
554
555
556 public boolean isExtendPath()
557 {
558 return this.extendPath;
559 }
560
561 }
562
563
564
565
566
567 private void checkKeys()
568 {
569 checkKeyOrUniques("Key", this.keys);
570 }
571
572
573
574
575
576 private void checkKeyrefs()
577 {
578 for (String fullPath : this.keyrefs.keySet())
579 {
580 Node node = this.keyrefs.get(fullPath);
581 String keyref = DocumentReader.getAttribute(node, "name");
582 String keyName = DocumentReader.getAttribute(node, "refer").replace("ots:", "");
583 Node key = null;
584 boolean keyFound = false;
585 Iterator<Node> iterator = this.keys.values().iterator();
586 while (!keyFound && iterator.hasNext())
587 {
588 key = iterator.next();
589 keyFound = keyName.equals(DocumentReader.getAttribute(key, "name"));
590 }
591 if (!keyFound)
592 {
593 System.out.println(
594 "Keyref " + keyref + " refers to non existing key " + DocumentReader.getAttribute(node, "refer") + ".");
595 }
596 String context = fullPath.substring(0, fullPath.lastIndexOf("."));
597 List<Node> elements = getSelectedElements(context, node);
598 if (elements.isEmpty())
599 {
600 elements = getSelectedElements(context, node);
601 System.out.println("Keyref " + keyref + " (" + getXpath(node) + ") not found among elements.");
602 }
603 else
604 {
605 for (Node selected : elements)
606 {
607 for (Node field : DocumentReader.getChildren(node, "xsd:field"))
608 {
609 String xpathFieldString = DocumentReader.getAttribute(field, "xpath");
610 boolean found = false;
611 for (String xpathField : xpathFieldString.split("\\|"))
612 {
613 if (xpathField.startsWith("@"))
614 {
615 xpathField = xpathField.substring(1);
616 if (hasElementAttribute(selected, xpathField))
617 {
618 found = true;
619
620 }
621 }
622 else if (selected.getNodeName().equals("xsd:simpleType")
623 || selected.getNodeName().equals("xsd:element"))
624 {
625 found = true;
626 }
627 }
628 if (!found)
629 {
630 System.out.println("Keyref " + keyref + " (" + getXpath(node) + ") points to non existing field '"
631 + xpathFieldString + "'.");
632 }
633 }
634 }
635 }
636 }
637 }
638
639
640
641
642
643 private void checkUniques()
644 {
645 checkKeyOrUniques("Unique", this.uniques);
646 }
647
648
649
650
651
652
653
654 private void checkKeyOrUniques(final String label, final Map<String, Node> map)
655 {
656 for (String fullPath : map.keySet())
657 {
658 Node node = map.get(fullPath);
659 String context = fullPath.substring(0, fullPath.lastIndexOf("."));
660 String element = DocumentReader.getAttribute(node, "name");
661 for (String selector : getXpath(node).split("\\|"))
662 {
663 String path;
664 Node selected = null;
665 if (!selector.startsWith(".//"))
666 {
667 path = context + "." + selector.replace("/", ".");
668 selected = getElement(path);
669 }
670 else
671 {
672
673 path = context + selector.replace(".//", ".{...}").replace("/", ".");
674 for (Entry<String, Node> entry : this.elements.entrySet())
675 {
676 String elementPath = entry.getKey();
677 if (elementPath.startsWith(context) && elementPath.endsWith(selector.substring(3).replace("/", ".")))
678 {
679 selected = entry.getValue();
680 break;
681 }
682 }
683 }
684 if (selected == null)
685 {
686 System.out.println(label + " " + element + " (" + path + ") not found among elements.");
687 }
688 else
689 {
690 for (Node field : DocumentReader.getChildren(node, "xsd:field"))
691 {
692 String xpathFieldString = DocumentReader.getAttribute(field, "xpath");
693 boolean found = false;
694 for (String xpathField : xpathFieldString.split("\\|"))
695 {
696 if (xpathField.startsWith("@"))
697 {
698 xpathField = xpathField.substring(1);
699 if (hasElementAttribute(selected, xpathField))
700 {
701 found = true;
702 }
703 }
704 }
705 if (!found)
706 {
707 System.out.println(label + " " + element + " (" + path + ") points to non existing field "
708 + xpathFieldString + ".");
709 }
710 }
711 }
712 }
713 }
714 }
715
716
717
718
719
720
721
722 private List<Node> getSelectedElements(final String context, final Node node)
723 {
724 List<Node> nodes = new ArrayList<>();
725 for (String selector : getXpath(node).split("\\|"))
726 {
727 Node selected = null;
728 if (!selector.startsWith(".//"))
729 {
730 selected = getElement(context + "." + selector.replace("/", "."));
731 }
732 else
733 {
734
735 for (Entry<String, Node> entry : this.elements.entrySet())
736 {
737 String elementPath = entry.getKey();
738 if (elementPath.startsWith(context) && elementPath.endsWith(selector.replace(".//", "").replace("/", ".")))
739 {
740 selected = entry.getValue();
741 break;
742 }
743 }
744 }
745 if (selected != null)
746 {
747 nodes.add(selected);
748 }
749 else
750 {
751 for (Entry<String, Node> entry : this.elements.entrySet())
752 {
753 if (!entry.getKey().startsWith("Ots.") && entry.getKey().endsWith(selector.replace(".//", "").replace("/", ".")))
754 {
755 nodes.add(entry.getValue());
756 }
757 else if (isType(entry.getValue(), selector.replace(".//", "").replace("/", ".")))
758 {
759 nodes.add(entry.getValue());
760 }
761 }
762 }
763 }
764 return nodes;
765 }
766
767
768
769
770
771
772 private String getXpath(final Node node)
773 {
774 Node child = DocumentReader.getChild(node, "xsd:selector");
775 String xpath = DocumentReader.getAttribute(child, "xpath");
776 xpath = xpath.replace("ots:", "");
777 return xpath;
778 }
779
780
781
782
783
784
785
786
787
788
789 private boolean hasElementAttribute(final Node node, final String name)
790 {
791 if (node.getNodeName().equals("xsd:complexType"))
792 {
793
794 return hasElementAttribute(node, name, null);
795 }
796
797 return hasElementAttribute(node, name, "xsd:complexType");
798 }
799
800
801
802
803
804
805
806
807
808
809
810 private boolean hasElementAttribute(final Node node, final String name, final String viaType)
811 {
812 Node via = viaType == null ? node : DocumentReader.getChild(node, viaType);
813 for (int childIndex = 0; childIndex < via.getChildNodes().getLength(); childIndex++)
814 {
815 Node child = via.getChildNodes().item(childIndex);
816 String childName = DocumentReader.getAttribute(child, "name");
817 if (child.getNodeName().equals("xsd:attribute") && name.equals(childName))
818 {
819 return true;
820 }
821 if (child.getNodeName().equals("xsd:sequence"))
822 {
823 boolean inSub = hasElementAttribute(child, name, null);
824 if (inSub)
825 {
826 return true;
827 }
828 }
829 if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
830 {
831 Node extension = DocumentReader.getChild(child, "xsd:extension");
832 String base = DocumentReader.getAttribute(extension, "base");
833 if (base != null)
834 {
835 Node baseNode = getType(base);
836 boolean has = hasElementAttribute(baseNode, name, null);
837 if (has)
838 {
839 return has;
840 }
841 }
842 boolean has = hasElementAttribute(extension, name, null);
843 if (has)
844 {
845 return has;
846 }
847 }
848 }
849 return false;
850 }
851
852
853
854
855
856 public Node getRoot()
857 {
858 return this.root;
859 }
860
861
862
863
864
865
866 public Node getElement(final String path)
867 {
868 return this.elements.get(path.replace("ots:", ""));
869 }
870
871
872
873
874
875
876 public Node getType(final String base)
877 {
878 return this.types.get(base.replace("ots:", ""));
879 }
880
881
882
883
884
885 public Map<String, Node> keys()
886 {
887 return new LinkedHashMap<>(this.keys);
888 }
889
890
891
892
893
894 public Map<String, Node> keyrefs()
895 {
896 return new LinkedHashMap<>(this.keyrefs);
897 }
898
899
900
901
902
903 public Map<String, Node> uniques()
904 {
905 return new LinkedHashMap<>(this.uniques);
906 }
907
908
909
910
911
912
913
914 public boolean isType(final Node node, final String path)
915 {
916 String name = DocumentReader.getAttribute(node, "name");
917 if (path.equals(name))
918 {
919 return true;
920 }
921 Node nodeUse = node;
922 if (nodeUse.getNodeName().equals("xsd:element"))
923 {
924 nodeUse = DocumentReader.getChild(node, "xsd:complexType");
925 if (nodeUse == null)
926 {
927 nodeUse = DocumentReader.getChild(node, "xsd:simpleType");
928 if (nodeUse == null)
929 {
930 return false;
931 }
932 }
933 }
934 for (int childIndex = 0; childIndex < nodeUse.getChildNodes().getLength(); childIndex++)
935 {
936 Node child = nodeUse.getChildNodes().item(childIndex);
937 if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
938 {
939 String base = null;
940 Node extension = DocumentReader.getChild(child, "xsd:extension");
941 if (extension != null)
942 {
943 base = DocumentReader.getAttribute(extension, "base");
944
945 }
946 Node restriction = DocumentReader.getChild(child, "xsd:restriction");
947 if (restriction != null)
948 {
949 base = DocumentReader.getAttribute(restriction, "base");
950 }
951 boolean isType = base.endsWith(path);
952 if (isType)
953 {
954 return isType;
955 }
956 if (base != null && !base.startsWith("xsd:"))
957 {
958 Node baseNode = getType(base);
959 if (baseNode != null && !baseNode.equals(nodeUse))
960 {
961 return isType(baseNode, path);
962 }
963 }
964 }
965 }
966 return false;
967 }
968
969 }