View Javadoc
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   * Reads the XML Schema in XSD format for OTS. This class contains various methods that the editor can use to present relevant
26   * structure and information to the user.
27   * <p>
28   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
29   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
30   * </p>
31   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
32   */
33  public class Schema
34  {
35  
36      /** Root OTS node. */
37      private Node root;
38  
39      /** List of read files. */
40      private final Set<String> readFiles = new LinkedHashSet<>();
41  
42      /** All loaded types, xsd:simpleType or xsd:complexType with name={name} attribute. */
43      private final Map<String, Node> types = new LinkedHashMap<>();
44  
45      /** Paths of all types that are sub-classed using xsd:extension, coupled to a set of the extending elements. */
46      private final Map<String, Set<String>> extendedTypes = new LinkedHashMap<>();
47  
48      /**
49       * Paths of types that are referred to by xsd:element or xsd:attribute with type={type}, coupled to a set of the referring
50       * elements.
51       */
52      private final Map<String, Set<String>> referredTypes = new LinkedHashMap<>();
53  
54      /** All loaded elements, path and node. */
55      private final Map<String, Node> elements = new LinkedHashMap<>();
56  
57      /** Paths of elements that are referred to by xsd:element with ref={ref}, coupled to a set of the referring elements. */
58      private final Map<String, Set<String>> referredElements = new LinkedHashMap<>();
59  
60      /** Documentation xsd:documentation stored at its path. */
61      private final Map<String, String> documentation = new LinkedHashMap<>();
62  
63      /** Nodes xsd:key. */
64      private final Map<String, Node> keys = new LinkedHashMap<>();
65  
66      /** Nodes xsd:keyref. */
67      private final Map<String, Node> keyrefs = new LinkedHashMap<>();
68  
69      /** Nodes xsd:unique. */
70      private final Map<String, Node> uniques = new LinkedHashMap<>();
71  
72      /** Reading queue. */
73      private final Queue<RecursionElement> queue = new LinkedList<>();
74  
75      /** Boolean to prevent infinite loop of self-queuing as a parent type is not found. */
76      private boolean blockLoop = false;
77  
78      /**
79       * Constructs the XML Schema information from a document.
80       * @param document main document, other files may be included from within the file.
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          // all elements with type={type} have that node stored, replace it with the referred node
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         // checks
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             // allTypes.forEach((str) -> System.out.println(" + " + str));
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             // allElements.forEach((str) -> System.out.println(" + " + str));
134         }
135 
136         checkKeys();
137         checkKeyrefs();
138         checkUniques();
139 
140         // allElements = new LinkedHashSet<>(this.elements.keySet());
141         // allElements.removeIf((key) -> !key.startsWith("Ots."));
142         // allElements.forEach((key) -> System.out.println(key));
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      * Reads the next node. If recursion is found, the node is ignored.<br>
164      * <br>
165      * If the node is xsd:extension, reading the extended type in place of the path is queued. If, however, the extended type is
166      * not yet loaded, reading this node is queued to be read again. The method will continue to read the parts of the node
167      * defined additional to the extended type. To prevent that this is read again later, this is captured in the later read,
168      * which will then be ignored.<br>
169      * <br>
170      * Extends the path for xsd:element's with either a name={name} or ref={ref} attribute. If the name is "Ots", this element
171      * is stored as the root for the whole schema. If there is a ref={ref} attribute, rather than reading the given node, the
172      * referred node is read at its place in the path.<br>
173      * <br>
174      * Also extends the path for an xsd:simpleType or xsd:complexType node, of it has a name={name} attribute.<br>
175      * <br>
176      * Finally, loops all the children of the node to read and processes them in the following manner:
177      * <ul>
178      * <li>#text nodes are ignored.</li>
179      * <li>xsd:include, xsd:attribute, xsd:element and xsd:documentation nodes are forwarded to dedicated methods.</li>
180      * <li>xsd:key, xsd:keyref and xsd:unique nodes are stored for later checks.</li>
181      * <li>All other child nodes are recursively read.</li>
182      * </ul>
183      * @param path node path.
184      * @param node xsd:attribute node.
185      * @param extendPath whether the path should be extended with this node.
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                     // this occurs if a type has a base, with the base being defined later in the XSD
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                 // this occurs if a type has a base, with the base being defined later in the XSD
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             // an xsd:element can not have a name and a ref attribute
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                  * There might be more exotic referring situations than this one. Here, we have an <xsd:element ref="Model">
247                  * pointing to a <xsd:element name="Model" type="ModelType" /> being typed by an <xsd:complexType
248                  * name="ModelType">.
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      * Checks for recursion. This is recognized as the same end of the path, is duplicated in an equal sub-path before that end.
320      * For example CarFollowingModel{.Socio}{.Socio} or CarFollowingModel{.Socio.Parent}{.Socio.Parent}.
321      * @param path node path.
322      * @return true if the path contains recursion.
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; // no dot in path
334             }
335             int toIndex = 2 * (dot + 1);
336             if (toIndex > sub.length())
337             {
338                 return false; // first dot beyond middle
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      * Queues reading the node at specified path. This is used to start all the reading, and to delay reading that is dependent
351      * on parent types to be loaded. When the read is queued as a parent type is not yet loaded, the path should not be extended
352      * upon the queued read. The path will have been extended in processing the child element itself.
353      * @param path node path.
354      * @param node node.
355      * @param extendPath whether the path should be extended with this node.
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      * Reads further from an included file.
364      * @param path node path.
365      * @param node xsd:include node.
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      * Returns the path, with separator at the end, relative to which an include in the node should be found.
387      * @param node node.
388      * @return path, with separator at the end, relative to which an include in the node should be found.
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      * Reads further from an element node.
404      * @param path node path.
405      * @param node xsd:element node.
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; // prevents reading the type now and later from queue
425             }
426             else
427             {
428                 queue(path + "." + DocumentReader.getAttribute(node, "name"), referred, false);
429             }
430         }
431         read(path, node, true);
432     }
433 
434     /**
435      * Reads further from an element node with a ref={ref} attribute. If the ref equals "xi:include" it is ignored, as this
436      * specifies that the XML file can include another XML file. It does not specify the schema further. If the referred type is
437      * not yet loaded, reading from this ref node is placed at the back of the queue.
438      * @param path node path.
439      * @param node xsd:element node.
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      * Reads further from an attribute node.
462      * @param path node path.
463      * @param node xsd:attribute node.
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      * Stores documentation at the current path.
478      * @param path node path.
479      * @param node xsd:attribute node.
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      * Read union node.
489      * @param path node path.
490      * @param node xsd:union node.
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      * Stores the information to read in a queue.
503      * <p>
504      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
505      * <br>
506      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
507      * </p>
508      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
509      */
510     private class RecursionElement
511     {
512         /** Path. */
513         private final String path;
514 
515         /** xsd:attribute node. */
516         private final Node node;
517 
518         /** Whether to extend the path. */
519         private final boolean extendPath;
520 
521         /**
522          * Constructor.
523          * @param path node path.
524          * @param node xsd:attribute node.
525          * @param extendPath whether the path should be extended with this node.
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          * Returns the path.
536          * @return path.
537          */
538         public String getPath()
539         {
540             return this.path;
541         }
542 
543         /**
544          * Returns the node.
545          * @return node.
546          */
547         public Node getNode()
548         {
549             return this.node;
550         }
551 
552         /**
553          * Returns whether to extend the path.
554          * @return whether to extend the path.
555          */
556         public boolean isExtendPath()
557         {
558             return this.extendPath;
559         }
560 
561     }
562 
563     /**
564      * Checks that all xsd:key refer to a loaded type with their xsd:selector node. And that all xsd:field nodes point to
565      * existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
566      */
567     private void checkKeys()
568     {
569         checkKeyOrUniques("Key", this.keys);
570     }
571 
572     /**
573      * Checks that all xsd:keyrefs refer to loaded keys. That they refer to a loaded type with their xsd:selector node. And that
574      * all xsd:field nodes point to existing attributes (@), values (.), or elements (ots:), in loaded types.
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                 System.out.println("Keyref " + keyref + " (" + getXpath(node) + ") not found among elements.");
601             }
602             else
603             {
604                 for (Node selected : elements)
605                 {
606                     for (Node field : DocumentReader.getChildren(node, "xsd:field"))
607                     {
608                         String xpathFieldString = DocumentReader.getAttribute(field, "xpath");
609                         boolean found = false;
610                         for (String xpathField : xpathFieldString.split("\\|"))
611                         {
612                             found = followXPath(selected, xpathField);
613                         }
614                         if (!found)
615                         {
616                             System.out.println("Keyref " + keyref + " (" + getXpath(node) + ") points to non existing field '"
617                                     + xpathFieldString + "'.");
618                         }
619                     }
620                 }
621             }
622         }
623     }
624 
625     /**
626      * Follows to xpath recursively to find a referred element.
627      * @param selected current node from which xpath is relative
628      * @param xpath xpath
629      * @return whether the element was found
630      */
631     private boolean followXPath(final Node selected, final String xpath)
632     {
633         if (xpath.startsWith("@"))
634         {
635             String xpathField = xpath.substring(1); // removes '@'
636             return hasElementAttribute(selected, xpathField);
637         }
638         if (xpath.equals(".")
639                 && (selected.getNodeName().equals("xsd:simpleType") || selected.getNodeName().equals("xsd:element")))
640         {
641             return true; // value is in element
642         }
643         int index = xpath.indexOf("/");
644         String name = index < 0 ? xpath : xpath.substring(0, index);
645         String remainder = index < 0 ? null : xpath.substring(index);
646         name = name.replace("ots:", "");
647         boolean found = false;
648         if (name.equals(DocumentReader.getAttribute(selected, "name")))
649         {
650             found = true;
651         }
652         else if (selected.getNodeName().equals("xsd:complexType") || selected.getNodeName().equals("xsd:sequence")
653                 || selected.getNodeName().equals("xsd:choice") || selected.getNodeName().equals("xsd:all")
654                 || selected.getNodeName().equals("xsd:element"))
655         {
656             for (int i = 0; i < selected.getChildNodes().getLength() && !found; i++)
657             {
658                 Node child = selected.getChildNodes().item(i);
659                 if (selected.getNodeName().equals("xsd:complexType") || selected.getNodeName().equals("xsd:sequence")
660                         || selected.getNodeName().equals("xsd:choice") || selected.getNodeName().equals("xsd:all")
661                         || selected.getNodeName().equals("xsd:element"))
662                 {
663                     found = followXPath(child, name);
664                 }
665             }
666         }
667         if (found && remainder != null)
668         {
669             for (int i = 0; i < selected.getChildNodes().getLength() && !found; i++)
670             {
671                 Node child = selected.getChildNodes().item(i);
672                 if (selected.getNodeName().equals("xsd:simpleType") || selected.getNodeName().equals("xsd:complexType")
673                         || selected.getNodeName().equals("xsd:element"))
674                 {
675                     found = followXPath(child, remainder);
676                 }
677             }
678         }
679         return found;
680     }
681 
682     /**
683      * Checks that all xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes point to
684      * existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
685      */
686     private void checkUniques()
687     {
688         checkKeyOrUniques("Unique", this.uniques);
689     }
690 
691     /**
692      * Checks that all xsd:key or xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes
693      * point to existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
694      * @param label "Key" or "Unique" for command line messaging.
695      * @param map map of nodes, either xsd:key or xsd:unique.
696      */
697     private void checkKeyOrUniques(final String label, final Map<String, Node> map)
698     {
699         for (String fullPath : map.keySet())
700         {
701             Node node = map.get(fullPath);
702             String context = fullPath.substring(0, fullPath.lastIndexOf("."));
703             String element = DocumentReader.getAttribute(node, "name");
704             for (String selector : getXpath(node).split("\\|"))
705             {
706                 String path;
707                 Node selected = null;
708                 if (!selector.startsWith(".//"))
709                 {
710                     path = context + "." + selector.replace("/", ".");
711                     selected = getElement(path);
712                 }
713                 else
714                 {
715                     // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
716                     path = context + selector.replace(".//", ".{...}").replace("/", ".");
717                     for (Entry<String, Node> entry : this.elements.entrySet())
718                     {
719                         String elementPath = entry.getKey();
720                         if (elementPath.startsWith(context) && elementPath.endsWith(selector.substring(3).replace("/", ".")))
721                         {
722                             selected = entry.getValue();
723                             break;
724                         }
725                     }
726                 }
727                 if (selected == null)
728                 {
729                     System.out.println(label + " " + element + " (" + path + ") not found among elements.");
730                 }
731                 else
732                 {
733                     for (Node field : DocumentReader.getChildren(node, "xsd:field"))
734                     {
735                         String xpathFieldString = DocumentReader.getAttribute(field, "xpath");
736                         boolean found = false;
737                         for (String xpathField : xpathFieldString.split("\\|"))
738                         {
739                             if (xpathField.startsWith("@"))
740                             {
741                                 xpathField = xpathField.substring(1); // removes '@'
742                                 if (hasElementAttribute(selected, xpathField))
743                                 {
744                                     found = true;
745                                 }
746                             }
747                         }
748                         if (!found)
749                         {
750                             System.out.println(label + " " + element + " (" + path + ") points to non existing field "
751                                     + xpathFieldString + ".");
752                         }
753                     }
754                 }
755             }
756         }
757     }
758 
759     /**
760      * Returns loaded elements referred to from an xsd:selector child of the given node.
761      * @param context context.
762      * @param node node (xsd:key, xsd:keyref or xsd:unique).
763      * @return elements referred to from an xsd:selector child of the given node.
764      */
765     private List<Node> getSelectedElements(final String context, final Node node)
766     {
767         List<Node> nodes = new ArrayList<>();
768         for (String selector : getXpath(node).split("\\|"))
769         {
770             Node selected = null;
771             if (!selector.startsWith(".//"))
772             {
773                 selected = getElement(context + "." + selector.replace("/", "."));
774             }
775             else
776             {
777                 // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
778                 for (Entry<String, Node> entry : this.elements.entrySet())
779                 {
780                     String elementPath = entry.getKey();
781                     if (elementPath.startsWith(context) && elementPath.endsWith(selector.replace(".//", "").replace("/", ".")))
782                     {
783                         selected = entry.getValue();
784                         break;
785                     }
786                 }
787             }
788             if (selected != null)
789             {
790                 nodes.add(selected);
791             }
792             else
793             {
794                 for (Entry<String, Node> entry : this.elements.entrySet())
795                 {
796                     if (!entry.getKey().startsWith("Ots.")
797                             && entry.getKey().endsWith(selector.replace(".//", "").replace("/", ".")))
798                     {
799                         nodes.add(entry.getValue());
800                     }
801                     else if (isType(entry.getValue(), selector.replace(".//", "").replace("/", ".")))
802                     {
803                         nodes.add(entry.getValue());
804                     }
805                 }
806             }
807         }
808         return nodes;
809     }
810 
811     /**
812      * Reads the xpath from an xsd:selector child of the given node.
813      * @param node node (xsd:key, xsd:keyref or xsd:unique).
814      * @return xpath from an xsd:selector child of the given node.
815      */
816     private String getXpath(final Node node)
817     {
818         Node child = DocumentReader.getChild(node, "xsd:selector");
819         String xpath = DocumentReader.getAttribute(child, "xpath");
820         xpath = xpath.replace("ots:", "");
821         return xpath;
822     }
823 
824     /**
825      * Returns whether the given node defines an element to have a specified attribute. This can be either because the node is
826      * xsd:complexType in which an xsd:attribute with the specified name is defined, because it has a child xsd:complexType
827      * meeting the same criteria, or because it is found in an underlying xsd:extension. For an xsd:extension, the attribute is
828      * sought in both the base type, and in the specified extension elements.
829      * @param node node.
830      * @param name attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
831      * @return whether the given node defines an element to have a specified attribute.
832      */
833     private boolean hasElementAttribute(final Node node, final String name)
834     {
835         if (node.getNodeName().equals("xsd:complexType"))
836         {
837             // node is a "xsd:complexType"
838             return hasElementAttribute(node, name, null);
839         }
840         // node is a "xsd:element" with a "xsd:complexType"
841         return hasElementAttribute(node, name, "xsd:complexType");
842     }
843 
844     /**
845      * Searches for an xsd:attribute in a given node. If no viaType is specified, this happens on the children of the given
846      * node. Otherwise, first a child node of viaType is taken, and the children nodes of that node are considered. If this
847      * method encounters an xsd:complexContent child that itself has a xsd:extension child, both the extended type and the
848      * specified extension elements, are considered.
849      * @param node node.
850      * @param name attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
851      * @param viaType viaType, can be used for recursion with or without an intermediate child layer.
852      * @return whether the given node defines an element to have a specified attribute.
853      */
854     private boolean hasElementAttribute(final Node node, final String name, final String viaType)
855     {
856         Node via = viaType == null ? node : DocumentReader.getChild(node, viaType);
857         for (int childIndex = 0; childIndex < via.getChildNodes().getLength(); childIndex++)
858         {
859             Node child = via.getChildNodes().item(childIndex);
860             String childName = DocumentReader.getAttribute(child, "name");
861             if (child.getNodeName().equals("xsd:attribute") && name.equals(childName))
862             {
863                 return true;
864             }
865             if (child.getNodeName().equals("xsd:sequence"))
866             {
867                 boolean inSub = hasElementAttribute(child, name, null);
868                 if (inSub)
869                 {
870                     return true;
871                 }
872             }
873             if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
874             {
875                 Node extension = DocumentReader.getChild(child, "xsd:extension");
876                 String base = DocumentReader.getAttribute(extension, "base");
877                 if (base != null)
878                 {
879                     Node baseNode = getType(base);
880                     boolean has = hasElementAttribute(baseNode, name, null); // null, referred types are already complex
881                     if (has)
882                     {
883                         return has;
884                     }
885                 }
886                 boolean has = hasElementAttribute(extension, name, null); // null, xsd:extension directly contains xsd:attribute
887                 if (has)
888                 {
889                     return has;
890                 }
891             }
892         }
893         return false;
894     }
895 
896     /**
897      * Get the root node.
898      * @return root node.
899      */
900     public Node getRoot()
901     {
902         return this.root;
903     }
904 
905     /**
906      * Returns the node for the given path.
907      * @param path path.
908      * @return type.
909      */
910     public Node getElement(final String path)
911     {
912         return this.elements.get(path.replace("ots:", ""));
913     }
914 
915     /**
916      * Returns the type, as pointed to by base={base}.
917      * @param base type.
918      * @return type, as pointed to by base={base}.
919      */
920     public Node getType(final String base)
921     {
922         return this.types.get(base.replace("ots:", ""));
923     }
924 
925     /**
926      * Returns the xsd:key and the paths where they are defined.
927      * @return xsd:key and the paths where they are defined.
928      */
929     public Map<String, Node> keys()
930     {
931         return new LinkedHashMap<>(this.keys);
932     }
933 
934     /**
935      * Returns the xsd:keyref and the paths where they are defined.
936      * @return xsd:keyref and the paths where they are defined.
937      */
938     public Map<String, Node> keyrefs()
939     {
940         return new LinkedHashMap<>(this.keyrefs);
941     }
942 
943     /**
944      * Returns the xsd:unique and the paths where they are defined.
945      * @return xsd:unique and the paths where they are defined.
946      */
947     public Map<String, Node> uniques()
948     {
949         return new LinkedHashMap<>(this.uniques);
950     }
951 
952     /**
953      * Return whether the given node is of the type.
954      * @param node node.
955      * @param path path of the type in dotted xpath notation, e.g. "SignalGroup.TrafficLight".
956      * @return whether the given node is of the type.
957      */
958     public boolean isType(final Node node, final String path)
959     {
960         String name = DocumentReader.getAttribute(node, "name");
961         if (path.equals(name))
962         {
963             return true;
964         }
965         Node nodeUse = node;
966         if (nodeUse.getNodeName().equals("xsd:element"))
967         {
968             nodeUse = DocumentReader.getChild(node, "xsd:complexType");
969             if (nodeUse == null)
970             {
971                 nodeUse = DocumentReader.getChild(node, "xsd:simpleType");
972                 if (nodeUse == null)
973                 {
974                     return false;
975                 }
976             }
977         }
978         for (int childIndex = 0; childIndex < nodeUse.getChildNodes().getLength(); childIndex++)
979         {
980             Node child = nodeUse.getChildNodes().item(childIndex);
981             if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
982             {
983                 String base = null;
984                 Node extension = DocumentReader.getChild(child, "xsd:extension");
985                 if (extension != null)
986                 {
987                     base = DocumentReader.getAttribute(extension, "base");
988 
989                 }
990                 Node restriction = DocumentReader.getChild(child, "xsd:restriction");
991                 if (restriction != null)
992                 {
993                     base = DocumentReader.getAttribute(restriction, "base");
994                 }
995                 boolean isType = base.endsWith(path);
996                 if (isType)
997                 {
998                     return isType;
999                 }
1000                 if (base != null && !base.startsWith("xsd:"))
1001                 {
1002                     Node baseNode = getType(base);
1003                     if (baseNode != null && !baseNode.equals(nodeUse))
1004                     {
1005                         return isType(baseNode, path);
1006                     }
1007                 }
1008             }
1009         }
1010         return false;
1011     }
1012 
1013 }