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 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 String; node path.
184      * @param node Node; xsd:attribute node.
185      * @param extendPath boolean; 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 String; node path.
322      * @return boolean; 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 String; node path.
354      * @param node Node; node.
355      * @param extendPath boolean; 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 String; node path.
365      * @param node 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; node.
388      * @return String; 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 String; node path.
405      * @param node 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 String; node path.
439      * @param node 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 String; node path.
463      * @param node 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 String; node path.
479      * @param node 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 String; node path.
490      * @param node 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 String; node path.
524          * @param node Node; xsd:attribute node.
525          * @param extendPath boolean; 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 String; path.
537          */
538         public String getPath()
539         {
540             return this.path;
541         }
542 
543         /**
544          * Returns the node.
545          * @return Node; node.
546          */
547         public Node getNode()
548         {
549             return this.node;
550         }
551 
552         /**
553          * Returns whether to extend the path.
554          * @return boolean; 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                 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); // removes '@'
616                                 if (hasElementAttribute(selected, xpathField))
617                                 {
618                                     found = true;
619 
620                                 }
621                             }
622                             else if (selected.getNodeName().equals("xsd:simpleType") // value is assumed given in element
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      * Checks that all xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes point to
641      * existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
642      */
643     private void checkUniques()
644     {
645         checkKeyOrUniques("Unique", this.uniques);
646     }
647 
648     /**
649      * Checks that all xsd:key or xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes
650      * point to existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
651      * @param label String; "Key" or "Unique" for command line messaging.
652      * @param map Map&lt;Node, String&gt;; map of nodes, either xsd:key or xsd:unique.
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                     // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
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); // removes '@'
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      * Returns loaded elements referred to from an xsd:selector child of the given node.
718      * @param context String; context.
719      * @param node Node; node (xsd:key, xsd:keyref or xsd:unique).
720      * @return List&lt;Node&gt;; elements referred to from an xsd:selector child of the given node.
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                 // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
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      * Reads the xpath from an xsd:selector child of the given node.
769      * @param node Node; node (xsd:key, xsd:keyref or xsd:unique).
770      * @return String; xpath from an xsd:selector child of the given node.
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      * Returns whether the given node defines an element to have a specified attribute. This can be either because the node is
782      * xsd:complexType in which an xsd:attribute with the specified name is defined, because it has a child xsd:complexType
783      * meeting the same criteria, or because it is found in an underlying xsd:extension. For an xsd:extension, the attribute is
784      * sought in both the base type, and in the specified extension elements.
785      * @param node Node; node.
786      * @param name String; attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
787      * @return whether the given node defines an element to have a specified attribute.
788      */
789     private boolean hasElementAttribute(final Node node, final String name)
790     {
791         if (node.getNodeName().equals("xsd:complexType"))
792         {
793             // node is a "xsd:complexType"
794             return hasElementAttribute(node, name, null);
795         }
796         // node is a "xsd:element" with a "xsd:complexType"
797         return hasElementAttribute(node, name, "xsd:complexType");
798     }
799 
800     /**
801      * Searches for an xsd:attribute in a given node. If no viaType is specified, this happens on the children of the given
802      * node. Otherwise, first a child node of viaType is taken, and the children nodes of that node are considered. If this
803      * method encounters an xsd:complexContent child that itself has a xsd:extension child, both the extended type and the
804      * specified extension elements, are considered.
805      * @param node Node; node.
806      * @param name String; attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
807      * @param viaType String; viaType, can be used for recursion with or without an intermediate child layer.
808      * @return whether the given node defines an element to have a specified attribute.
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); // null, referred types are already complex
837                     if (has)
838                     {
839                         return has;
840                     }
841                 }
842                 boolean has = hasElementAttribute(extension, name, null); // null, xsd:extension directly contains xsd:attribute
843                 if (has)
844                 {
845                     return has;
846                 }
847             }
848         }
849         return false;
850     }
851 
852     /**
853      * Get the root node.
854      * @return Node; root node.
855      */
856     public Node getRoot()
857     {
858         return this.root;
859     }
860 
861     /**
862      * Returns the node for the given path.
863      * @param path String; path.
864      * @return Node; type.
865      */
866     public Node getElement(final String path)
867     {
868         return this.elements.get(path.replace("ots:", ""));
869     }
870 
871     /**
872      * Returns the type, as pointed to by base={base}.
873      * @param base String; type.
874      * @return String; type, as pointed to by base={base}.
875      */
876     public Node getType(final String base)
877     {
878         return this.types.get(base.replace("ots:", ""));
879     }
880 
881     /**
882      * Returns the xsd:key and the paths where they are defined.
883      * @return Map&lt;String, Node&gt;; xsd:key and the paths where they are defined.
884      */
885     public Map<String, Node> keys()
886     {
887         return new LinkedHashMap<>(this.keys);
888     }
889 
890     /**
891      * Returns the xsd:keyref and the paths where they are defined.
892      * @return Map&lt;String, Node&gt;; xsd:keyref and the paths where they are defined.
893      */
894     public Map<String, Node> keyrefs()
895     {
896         return new LinkedHashMap<>(this.keyrefs);
897     }
898 
899     /**
900      * Returns the xsd:unique and the paths where they are defined.
901      * @return Map&lt;String, Node&gt;; xsd:unique and the paths where they are defined.
902      */
903     public Map<String, Node> uniques()
904     {
905         return new LinkedHashMap<>(this.uniques);
906     }
907 
908     /**
909      * Return whether the given node is of the type.
910      * @param node Node; node.
911      * @param path String; path of the type in dotted xpath notation, e.g. "SignalGroup.TrafficLight".
912      * @return boolean; whether the given node is of the type.
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 }