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.Optional;
15  import java.util.Queue;
16  import java.util.Set;
17  
18  import javax.xml.parsers.ParserConfigurationException;
19  
20  import org.djutils.exceptions.Throw;
21  import org.opentrafficsim.base.OtsRuntimeException;
22  import org.opentrafficsim.base.logger.Logger;
23  import org.w3c.dom.Document;
24  import org.w3c.dom.Node;
25  import org.xml.sax.SAXException;
26  
27  /**
28   * Reads the XML Schema in XSD format for OTS. This class contains various methods that the editor can use to present relevant
29   * structure and information to the user.
30   * <p>
31   * This class performs various checks on XSD consistency. To obtain a list of inconsistencies encountered, allow trace level
32   * logging.
33   * <p>
34   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
35   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
36   * </p>
37   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
38   */
39  public class Schema
40  {
41  
42      /** Root OTS node. */
43      private Node root;
44  
45      /** List of read files. */
46      private final Set<String> readFiles = new LinkedHashSet<>();
47  
48      /** All loaded types, xsd:simpleType or xsd:complexType with name={name} attribute. */
49      private final Map<String, Node> types = new LinkedHashMap<>();
50  
51      /** Paths of all types that are sub-classed using xsd:extension, coupled to a set of the extending elements. */
52      private final Map<String, Set<String>> extendedTypes = new LinkedHashMap<>();
53  
54      /**
55       * Paths of types that are referred to by xsd:element or xsd:attribute with type={type}, coupled to a set of the referring
56       * elements.
57       */
58      private final Map<String, Set<String>> referredTypes = new LinkedHashMap<>();
59  
60      /** All loaded elements, path and node. */
61      private final Map<String, Node> elements = new LinkedHashMap<>();
62  
63      /** Paths of elements that are referred to by xsd:element with ref={ref}, coupled to a set of the referring elements. */
64      private final Map<String, Set<String>> referredElements = new LinkedHashMap<>();
65  
66      /** Documentation xsd:documentation stored at its path. */
67      private final Map<String, String> documentation = new LinkedHashMap<>();
68  
69      /** Nodes xsd:key. */
70      private final Map<String, Node> keys = new LinkedHashMap<>();
71  
72      /** Nodes xsd:keyref. */
73      private final Map<String, Node> keyrefs = new LinkedHashMap<>();
74  
75      /** Nodes xsd:unique. */
76      private final Map<String, Node> uniques = new LinkedHashMap<>();
77  
78      /** Reading queue. */
79      private final Queue<RecursionElement> queue = new LinkedList<>();
80  
81      /** Boolean to prevent infinite loop of self-queuing as a parent type is not found. */
82      private boolean blockLoop = false;
83  
84      /**
85       * Constructs the XML Schema information from a document.
86       * @param document main document, other files may be included from within the file.
87       */
88      public Schema(final Document document)
89      {
90          this.readFiles.add(document.getDocumentURI());
91  
92          queue("", document, true);
93          while (!this.queue.isEmpty())
94          {
95              RecursionElement next = this.queue.poll();
96              read(next.path(), next.node(), next.extendPath());
97          }
98  
99          // all elements with type={type} have that node stored, replace it with the referred node
100         for (Entry<String, Node> entry : this.elements.entrySet())
101         {
102             Optional<String> referredTypeName = DocumentReader.getAttribute(entry.getValue(), "type");
103             if (referredTypeName.isPresent() && !referredTypeName.get().startsWith("xsd:"))
104             {
105                 Node referredType = getType(referredTypeName.get()).get();
106                 entry.setValue(referredType);
107             }
108         }
109 
110         // checks
111         Set<String> allTypes = new LinkedHashSet<>(this.types.keySet());
112         for (String str : this.extendedTypes.keySet())
113         {
114             allTypes.removeIf((val) -> val.startsWith(str));
115         }
116         for (String str : this.referredTypes.keySet())
117         {
118             allTypes.removeIf((val) -> val.startsWith(str));
119         }
120         if (!allTypes.isEmpty())
121         {
122             Logger.ots().trace("{} types are defined but never extended or referred to.", allTypes.size());
123             // allTypes.forEach((str) -> Logger.ots().trace(" + {}", str));
124         }
125 
126         Set<String> allElements = new LinkedHashSet<>(this.elements.keySet());
127         for (String str : this.referredElements.keySet())
128         {
129             allElements.removeIf((val) -> val.startsWith(str));
130         }
131         for (String str : this.types.keySet())
132         {
133             allElements.removeIf((val) -> val.startsWith(str));
134         }
135         allElements.removeIf((path) -> path.startsWith("Ots"));
136         if (!allElements.isEmpty())
137         {
138             Logger.ots().trace("{} elements are defined but never referred to, nor are they a type.", allElements.size());
139             // allElements.forEach((str) -> Logger.ots().trace(" + {}", str));
140         }
141 
142         checkKeys();
143         checkKeyrefs();
144         checkUniques();
145 
146         // allElements = new LinkedHashSet<>(this.elements.keySet());
147         // allElements.removeIf((key) -> !key.startsWith("Ots."));
148         // allElements.forEach((key) -> Logger.ots().trace(key));
149 
150         Logger.ots().trace("Root found as '{}'.", DocumentReader.getAttribute(this.getRoot(), "name").get());
151         Logger.ots().trace("Read {} files.", this.readFiles.size());
152         Logger.ots().trace("Read {} elements.", this.elements.size());
153         Logger.ots().trace("Read {} types.", this.types.size());
154         Logger.ots().trace("Read {} extended types.", this.extendedTypes.size());
155         Logger.ots().trace("Read {} documentations.", this.documentation.size());
156         Logger.ots().trace("Read {} keys.", this.keys.size());
157         Logger.ots().trace("Read {} keyrefs.", this.keyrefs.size());
158         Logger.ots().trace("Read {} uniques.", this.uniques.size());
159         for (String type : this.extendedTypes.keySet())
160         {
161             if (!this.types.containsKey(type) && !type.startsWith("xsd:"))
162             {
163                 Logger.ots().trace("Type '{}' is extended but was not found.", type);
164             }
165         }
166     }
167 
168     /**
169      * Reads the next node. If recursion is found, the node is ignored.<br>
170      * <br>
171      * If the node is xsd:extension, reading the extended type in place of the path is queued. If, however, the extended type is
172      * not yet loaded, reading this node is queued to be read again. The method will continue to read the parts of the node
173      * defined additional to the extended type. To prevent that this is read again later, this is captured in the later read,
174      * which will then be ignored.<br>
175      * <br>
176      * Extends the path for xsd:element's with either a name={name} or ref={ref} attribute. If the name is "Ots", this element
177      * is stored as the root for the whole schema. If there is a ref={ref} attribute, rather than reading the given node, the
178      * referred node is read at its place in the path.<br>
179      * <br>
180      * Also extends the path for an xsd:simpleType or xsd:complexType node, if it has a name={name} attribute.<br>
181      * <br>
182      * Finally, loops all the children of the node to read and processes them in the following manner:
183      * <ul>
184      * <li>#text nodes are ignored.</li>
185      * <li>xsd:include, xsd:attribute, xsd:element and xsd:documentation nodes are forwarded to dedicated methods.</li>
186      * <li>xsd:key, xsd:keyref and xsd:unique nodes are stored for later checks.</li>
187      * <li>All other child nodes are recursively read.</li>
188      * </ul>
189      * @param path node path.
190      * @param node xsd:attribute node.
191      * @param extendPath whether the path should be extended with this node.
192      */
193     private void read(final String path, final Node node, final boolean extendPath)
194     {
195         if (recursion(path))
196         {
197             Logger.ots().trace("Recursion found at {}, further expansion is halted.", path);
198             return;
199         }
200 
201         if (node.getNodeName().equals("xsd:extension"))
202         {
203             String base = DocumentReader.getAttribute(node, "base").get().replace("ots:", "");
204             if (!base.startsWith("xsd:"))
205             {
206                 Optional<Node> baseNode = getType(base);
207                 if (baseNode.isEmpty())
208                 {
209                     if (this.blockLoop)
210                     {
211                         return;
212                     }
213                     this.blockLoop = true;
214                     // this occurs if a type has a base, with the base being defined later in the XSD
215                     queue(path, node, false);
216                 }
217                 else
218                 {
219                     queue(path, baseNode.get(), false);
220                 }
221             }
222             if (this.extendedTypes.containsKey(base) && this.extendedTypes.get(base).contains(path))
223             {
224                 // this occurs if a type has a base, with the base being defined later in the XSD
225                 // do not read what is additional to the base, as that was done previously and this is the queued base (re)read
226                 return;
227             }
228             this.extendedTypes.computeIfAbsent(base, (key) -> new LinkedHashSet<String>()).add(path);
229         }
230         this.blockLoop = false;
231 
232         String nextPath = path;
233         Node nextNode = node;
234         if (node.getNodeName().equals("xsd:element") && node.hasAttributes())
235         {
236             // an xsd:element can not have a name and a ref attribute
237             Optional<String> name = DocumentReader.getAttribute(node, "name");
238             if (name.isPresent())
239             {
240                 if (name.get().equals("Ots"))
241                 {
242                     this.root = node;
243                 }
244                 nextPath = extendPath ? (nextPath.isEmpty() ? name.get() : nextPath + "." + name) : nextPath;
245                 this.elements.put(nextPath, node);
246             }
247             String ref = DocumentReader.getAttribute(node, "ref").orElse(null);
248             if (ref != null)
249             {
250                 ref = ref.replace("ots:", "");
251                 nextNode = getElement(ref).get();
252                 /*
253                  * There might be more exotic referring situations than this one. Here, we have an <xsd:element ref="Model">
254                  * pointing to a <xsd:element name="Model" type="ModelType" /> being typed by an <xsd:complexType
255                  * name="ModelType">.
256                  */
257                 if (DocumentReader.getAttribute(nextNode, "type").isPresent())
258                 {
259                     element(path, nextNode);
260                 }
261                 nextPath = extendPath ? (nextPath.isEmpty() ? ref : nextPath + "." + ref) : nextPath;
262                 this.elements.put(nextPath, nextNode);
263                 Throw.whenNull(nextNode, "Element %s refers to a type that was not loaded in first pass.", nextPath);
264             }
265         }
266 
267         Optional<String> name = DocumentReader.getAttribute(nextNode, "name");
268 
269         String nodeName = nextNode.getNodeName();
270         if (name.isPresent() && (nodeName.equals("xsd:complexType") || nodeName.equals("xsd:simpleType")))
271         {
272             this.types.put(name.get(), nextNode);
273             nextPath = extendPath ? (nextPath.isEmpty() ? name.get() : nextPath + "." + name.get()) : nextPath;
274         }
275 
276         if (!nextNode.hasChildNodes())
277         {
278             return;
279         }
280         for (int childIndex = 0; childIndex < nextNode.getChildNodes().getLength(); childIndex++)
281         {
282             Node child = nextNode.getChildNodes().item(childIndex);
283             switch (child.getNodeName())
284             {
285                 case "#text":
286                     break;
287                 case "xsd:include":
288                     include(nextPath, child);
289                     break;
290                 case "xsd:element":
291                     element(nextPath, child);
292                     break;
293                 case "xsd:attribute":
294                     attribute(nextPath, child);
295                     break;
296                 case "xsd:documentation":
297                     documentation(nextPath, child);
298                     break;
299                 case "xsd:union":
300                     union(nextPath, child);
301                     break;
302                 case "xsd:key":
303                     if (nextPath.startsWith("Ots"))
304                     {
305                         this.keys.put(nextPath + "." + DocumentReader.getAttribute(child, "name").get(), child);
306                     }
307                     break;
308                 case "xsd:keyref":
309                     if (nextPath.startsWith("Ots"))
310                     {
311                         this.keyrefs.put(nextPath + "." + DocumentReader.getAttribute(child, "name").get(), child);
312                     }
313                     break;
314                 case "xsd:unique":
315                     if (nextPath.startsWith("Ots"))
316                     {
317                         this.uniques.put(nextPath + "." + DocumentReader.getAttribute(child, "name").get(), child);
318                     }
319                     break;
320                 default:
321                     read(nextPath, child, true);
322             }
323         }
324     }
325 
326     /**
327      * Checks for recursion. This is recognized as the same end of the path, is duplicated in an equal sub-path before that end.
328      * For example CarFollowingModel{.Socio}{.Socio} or CarFollowingModel{.Socio.Parent}{.Socio.Parent}.
329      * @param path node path.
330      * @return true if the path contains recursion.
331      */
332     private boolean recursion(final String path)
333     {
334         StringBuffer sub = (new StringBuffer(path)).reverse();
335         int fromIndex = 0;
336         while (fromIndex < sub.length())
337         {
338             int dot = sub.indexOf(".", fromIndex);
339             if (dot < 0)
340             {
341                 return false; // no dot in path
342             }
343             int toIndex = 2 * (dot + 1);
344             if (toIndex > sub.length())
345             {
346                 return false; // first dot beyond middle
347             }
348             if (sub.substring(0, dot + 1).equals(sub.substring(dot + 1, toIndex)))
349             {
350                 return true;
351             }
352             fromIndex = dot + 1;
353         }
354         return false;
355     }
356 
357     /**
358      * Queues reading the node at specified path. This is used to start all the reading, and to delay reading that is dependent
359      * 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
360      * upon the queued read. The path will have been extended in processing the child element itself.
361      * @param path node path.
362      * @param node node.
363      * @param extendPath whether the path should be extended with this node.
364      */
365     private void queue(final String path, final Node node, final boolean extendPath)
366     {
367         this.queue.add(new RecursionElement(path, node, extendPath));
368     }
369 
370     /**
371      * Reads further from an included file.
372      * @param path node path.
373      * @param node xsd:include node.
374      */
375     private void include(final String path, final Node node)
376     {
377         String schemaLocation = DocumentReader.getAttribute(node, "schemaLocation").get();
378         String schemaPath = folder(node) + schemaLocation;
379         if (!this.readFiles.add(schemaPath))
380         {
381             return;
382         }
383         try
384         {
385             read(path, DocumentReader.open(new URI(schemaPath)), true);
386         }
387         catch (SAXException | IOException | ParserConfigurationException | URISyntaxException e)
388         {
389             throw new OtsRuntimeException("Unable to find resource " + folder(node) + schemaLocation);
390         }
391     }
392 
393     /**
394      * Returns the path, with separator at the end, relative to which an include in the node should be found.
395      * @param node node.
396      * @return path, with separator at the end, relative to which an include in the node should be found.
397      */
398     private String folder(final Node node)
399     {
400         String uri = node.getBaseURI();
401         if (uri == null)
402         {
403             return "";
404         }
405         int a = uri.lastIndexOf("\\");
406         int b = uri.lastIndexOf("/");
407         return uri.substring(0, (a > b ? a : b) + 1);
408     }
409 
410     /**
411      * Reads further from an element node.
412      * @param path node path.
413      * @param node xsd:element node.
414      */
415     private void element(final String path, final Node node)
416     {
417         if (DocumentReader.getAttribute(node, "ref").isPresent())
418         {
419             ref(path, node);
420             return;
421         }
422         String type = DocumentReader.getAttribute(node, "type").orElse(null);
423         if (type != null && !type.startsWith("xsd:"))
424         {
425             type = type.replace("ots:", "");
426             this.referredTypes.computeIfAbsent(type, (key) -> new LinkedHashSet<>())
427                     .add(path.isEmpty() ? DocumentReader.getAttribute(node, "name").get()
428                             : path + "." + DocumentReader.getAttribute(node, "name").get());
429             Optional<Node> referred = getType(type);
430             if (referred.isEmpty())
431             {
432                 queue(path, node, true);
433                 return; // prevents reading the type now and later from queue
434             }
435             else
436             {
437                 queue(path + "." + DocumentReader.getAttribute(node, "name").get(), referred.get(), false);
438             }
439         }
440         read(path, node, true);
441     }
442 
443     /**
444      * Reads further from an element node with a ref={ref} attribute. If the ref equals "xi:include" it is ignored, as this
445      * specifies that the XML file can include another XML file. It does not specify the schema further. If the referred type is
446      * not yet loaded, reading from this ref node is placed at the back of the queue.
447      * @param path node path.
448      * @param node xsd:element node.
449      */
450     private void ref(final String path, final Node node)
451     {
452         String ref = DocumentReader.getAttribute(node, "ref").get().replace("ots:", "");
453         if (ref.equals("xi:include"))
454         {
455             return;
456         }
457         this.referredElements.computeIfAbsent(ref, (key) -> new LinkedHashSet<>()).add(path);
458         Optional<Node> refNode = getElement(ref);
459         if (refNode.isEmpty())
460         {
461             queue(path, node, true);
462         }
463         else
464         {
465             read(path, node, true);
466         }
467     }
468 
469     /**
470      * Reads further from an attribute node.
471      * @param path node path.
472      * @param node xsd:attribute node.
473      */
474     private void attribute(final String path, final Node node)
475     {
476         String type = DocumentReader.getAttribute(node, "type").orElse(null);
477         if (type != null)
478         {
479             type = DocumentReader.getAttribute(node, "type").get().replace("ots:", "");
480             String name = DocumentReader.getAttribute(node, "name").get();
481             this.referredTypes.computeIfAbsent(type, (key) -> new LinkedHashSet<>()).add(path + "." + name);
482         }
483         read(path, node, true);
484     }
485 
486     /**
487      * Stores documentation at the current path.
488      * @param path node path.
489      * @param node xsd:attribute node.
490      */
491     private void documentation(final String path, final Node node)
492     {
493         Optional<Node> doc = DocumentReader.getChild(node, "#text");
494         if (doc.isPresent())
495         {
496             this.documentation.put(path, doc.get().getNodeValue().trim().replaceAll("\r\n", " ").replaceAll("\n", " ")
497                     .replaceAll("\r", " ").replace("  ", ""));
498         }
499     }
500 
501     /**
502      * Read union node.
503      * @param path node path.
504      * @param node xsd:union node.
505      */
506     private void union(final String path, final Node node)
507     {
508         /*
509          * Note: unions can only consist of simple types (with only a regular value). Therefore this pertains to valid values
510          * only, and not to children and attributes. Therefore the memberTypes do not need to be read in place of the node.
511          */
512         for (String type : DocumentReader.getAttribute(node, "memberTypes").get().split(" "))
513         {
514             this.referredTypes.computeIfAbsent(type.replace("ots:", ""), (key) -> new LinkedHashSet<>()).add(path);
515         }
516         read(path, node, true);
517     }
518 
519     /**
520      * Stores the information to read in a queue.
521      * <p>
522      * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
523      * <br>
524      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
525      * </p>
526      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
527      * @param path node path.
528      * @param node xsd:attribute node.
529      * @param extendPath whether the path should be extended with this node.
530      */
531     private record RecursionElement(String path, Node node, boolean extendPath)
532     {
533     }
534 
535     /**
536      * Checks that all xsd:key refer to a loaded type with their xsd:selector node. And that all xsd:field nodes point to
537      * existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
538      */
539     private void checkKeys()
540     {
541         checkKeyOrUniques("Key", this.keys);
542     }
543 
544     /**
545      * Checks that all xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes point to
546      * existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
547      */
548     private void checkUniques()
549     {
550         checkKeyOrUniques("Unique", this.uniques);
551     }
552 
553     /**
554      * Checks that all xsd:key or xsd:unique refer to a loaded type with their xsd:selector node. And that all xsd:field nodes
555      * point to existing attributes in loaded types. This method assumes only attributes (@) and no elements (ots:) are checked.
556      * @param label "Key" or "Unique" for log messaging.
557      * @param map map of nodes, either xsd:key or xsd:unique.
558      */
559     private void checkKeyOrUniques(final String label, final Map<String, Node> map)
560     {
561         for (String fullPath : map.keySet())
562         {
563             Node node = map.get(fullPath);
564             String context = fullPath.substring(0, fullPath.lastIndexOf("."));
565             String element = DocumentReader.getAttribute(node, "name").get();
566             for (String selector : getXpath(node).split("\\|"))
567             {
568                 String path;
569                 Node selected = null;
570                 if (!selector.startsWith(".//"))
571                 {
572                     path = context + "." + selector.replace("/", ".");
573                     selected = getElement(path).orElse(null);
574                 }
575                 else
576                 {
577                     // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
578                     path = context + selector.replace(".//", ".{...}").replace("/", ".");
579                     for (Entry<String, Node> entry : this.elements.entrySet())
580                     {
581                         String elementPath = entry.getKey();
582                         if (elementPath.startsWith(context) && elementPath.endsWith(selector.substring(3).replace("/", ".")))
583                         {
584                             selected = entry.getValue();
585                             break;
586                         }
587                     }
588                 }
589                 if (selected == null)
590                 {
591                     Logger.ots().trace("{} {} ({}) not found among elements.", label, element, path);
592                 }
593                 else
594                 {
595                     for (Node field : DocumentReader.getChildren(node, "xsd:field"))
596                     {
597                         String xpathFieldString = DocumentReader.getAttribute(field, "xpath").get();
598                         boolean found = false;
599                         for (String xpathField : xpathFieldString.split("\\|"))
600                         {
601                             if (xpathField.startsWith("@"))
602                             {
603                                 xpathField = xpathField.substring(1); // removes '@'
604                                 if (hasElementAttribute(selected, xpathField))
605                                 {
606                                     found = true;
607                                 }
608                             }
609                             else
610                             {
611                                 Logger.ots().trace("Field {} in {} {} not checked.", xpathField, label.toLowerCase(), element);
612                             }
613                         }
614                         if (!found)
615                         {
616                             Logger.ots().trace("{} {} ({}) points to non existing field {}.", label, element, path,
617                                     xpathFieldString);
618                         }
619                     }
620                 }
621             }
622         }
623     }
624 
625     /**
626      * Checks that all xsd:keyrefs refer to loaded keys. That they refer to a loaded type with their xsd:selector node. And that
627      * all xsd:field nodes point to existing attributes (@), values (.), or elements (ots:), in loaded types.
628      */
629     private void checkKeyrefs()
630     {
631         for (String fullPath : this.keyrefs.keySet())
632         {
633             Node node = this.keyrefs.get(fullPath);
634             String keyref = DocumentReader.getAttribute(node, "name").get();
635             String keyName = DocumentReader.getAttribute(node, "refer").get().replace("ots:", "");
636             Node key = null;
637             boolean keyFound = false;
638             Iterator<Node> iterator = this.keys.values().iterator();
639             while (!keyFound && iterator.hasNext())
640             {
641                 key = iterator.next();
642                 keyFound = keyName.equals(DocumentReader.getAttribute(key, "name").orElse(null));
643             }
644             if (!keyFound)
645             {
646                 Logger.ots().trace("Keyref {} refers to non existing key {}.", keyref,
647                         DocumentReader.getAttribute(node, "refer").orElse(null));
648             }
649             String context = fullPath.substring(0, fullPath.lastIndexOf("."));
650             List<Node> elementList = getSelectedElements(context, node);
651             if (elementList.isEmpty())
652             {
653                 Logger.ots().trace("Keyref {} ({}) not found among elements.", keyref, getXpath(node));
654             }
655             else
656             {
657                 for (int i = 0; i < elementList.size(); i++)
658                 {
659                     Node selected = elementList.get(i);
660                     for (Node field : DocumentReader.getChildren(node, "xsd:field"))
661                     {
662                         String xpathFieldString = DocumentReader.getAttribute(field, "xpath").get();
663                         String[] xpathFieldValues = xpathFieldString.split("\\|");
664                         String xpathField = xpathFieldValues.length == 1 ? xpathFieldValues[0] : xpathFieldValues[i];
665                         if (!followXPath(selected, xpathField))
666                         {
667                             Logger.ots().trace("Keyref {} ({}) points to non existing field {}.", keyref,
668                                     getXpath(node).split("\\|")[i], xpathField);
669                         }
670                     }
671                 }
672             }
673         }
674     }
675 
676     /**
677      * Follows to xpath recursively to find a referred element.
678      * @param selected current node from which xpath is relative
679      * @param xpath xpath
680      * @return whether the element was found
681      */
682     private boolean followXPath(final Node selected, final String xpath)
683     {
684         if (xpath.startsWith("@"))
685         {
686             String xpathField = xpath.substring(1); // removes '@'
687             return hasElementAttribute(selected, xpathField);
688         }
689         String selectedName = selected.getNodeName();
690         if (xpath.equals(".") && (selectedName.equals("xsd:simpleType") || selectedName.equals("xsd:element")))
691         {
692             return true; // value is in element
693         }
694         int index = xpath.indexOf("/");
695         String name = index < 0 ? xpath : xpath.substring(0, index);
696         String remainder = index < 0 ? null : xpath.substring(index);
697         name = name.replace("ots:", "");
698         boolean found = false;
699         if (name.equals(DocumentReader.getAttribute(selected, "name").orElse(null)))
700         {
701             found = true;
702         }
703         else if (selectedName.equals("xsd:complexType") || selectedName.equals("xsd:sequence")
704                 || selectedName.equals("xsd:choice") || selectedName.equals("xsd:all") || selectedName.equals("xsd:element"))
705         {
706             for (int i = 0; i < selected.getChildNodes().getLength() && !found; i++)
707             {
708                 Node child = selected.getChildNodes().item(i);
709                 found = followXPath(child, name);
710             }
711         }
712         if (found && remainder != null)
713         {
714             for (int i = 0; i < selected.getChildNodes().getLength() && !found; i++)
715             {
716                 Node child = selected.getChildNodes().item(i);
717                 if (selectedName.equals("xsd:simpleType") || selectedName.equals("xsd:complexType")
718                         || selectedName.equals("xsd:element"))
719                 {
720                     found = followXPath(child, remainder);
721                 }
722             }
723         }
724         return found;
725     }
726 
727     /**
728      * Returns loaded elements referred to from an xsd:selector child of the given node. A list is returned as multiple elements
729      * may be referred to in the selector string split by "|".
730      * @param context context.
731      * @param node node (xsd:key, xsd:keyref or xsd:unique).
732      * @return elements referred to from an xsd:selector child of the given node.
733      */
734     private List<Node> getSelectedElements(final String context, final Node node)
735     {
736         List<Node> nodes = new ArrayList<>();
737         for (String selector : getXpath(node).split("\\|"))
738         {
739             Node selected = null;
740             if (!selector.startsWith(".//"))
741             {
742                 selected = getElement(context + "." + selector.replace("/", ".")).orElse(null);
743             }
744             else
745             {
746                 // do it the hard way for if there are intermediate layers, e.g. Ots.{...}.GtuTypes.GtuType
747                 for (Entry<String, Node> entry : this.elements.entrySet())
748                 {
749                     String elementPath = entry.getKey();
750                     if (elementPath.startsWith(context) && elementPath.endsWith(selector.replace(".//", "").replace("/", ".")))
751                     {
752                         selected = entry.getValue();
753                         break;
754                     }
755                 }
756             }
757             if (selected != null)
758             {
759                 nodes.add(selected);
760             }
761             else
762             {
763                 for (Entry<String, Node> entry : this.elements.entrySet())
764                 {
765                     if (!entry.getKey().startsWith("Ots.")
766                             && entry.getKey().endsWith(selector.replace(".//", "").replace("/", ".")))
767                     {
768                         nodes.add(entry.getValue());
769                         break;
770                     }
771                     else if (isType(entry.getValue(), selector.replace(".//", "").replace("/", ".")))
772                     {
773                         nodes.add(entry.getValue());
774                         break;
775                     }
776                 }
777             }
778         }
779         return nodes;
780     }
781 
782     /**
783      * Reads the xpath from an xsd:selector child of the given node.
784      * @param node node (xsd:key, xsd:keyref or xsd:unique).
785      * @return xpath from an xsd:selector child of the given node.
786      */
787     private String getXpath(final Node node)
788     {
789         Node child = DocumentReader.getChild(node, "xsd:selector").get();
790         String xpath = DocumentReader.getAttribute(child, "xpath").get();
791         xpath = xpath.replace("ots:", "");
792         return xpath;
793     }
794 
795     /**
796      * Returns whether the given node defines an element to have a specified attribute. This can be either because the node is
797      * xsd:complexType in which an xsd:attribute with the specified name is defined, because it has a child xsd:complexType
798      * meeting the same criteria, or because it is found in an underlying xsd:extension. For an xsd:extension, the attribute is
799      * sought in both the base type, and in the specified extension elements.
800      * @param node node.
801      * @param name attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
802      * @return whether the given node defines an element to have a specified attribute.
803      */
804     private boolean hasElementAttribute(final Node node, final String name)
805     {
806         if (node.getNodeName().equals("xsd:complexType"))
807         {
808             // node is a "xsd:complexType"
809             return hasElementAttribute(node, name, null);
810         }
811         // node is a "xsd:element" with a "xsd:complexType"
812         return hasElementAttribute(node, name, "xsd:complexType");
813     }
814 
815     /**
816      * Searches for an xsd:attribute in a given node. If no viaType is specified, this happens on the children of the given
817      * node. Otherwise, first a child node of viaType is taken, and the children nodes of that node are considered. If this
818      * method encounters an xsd:complexContent child that itself has a xsd:extension child, both the extended type and the
819      * specified extension elements are considered.
820      * @param node node.
821      * @param name attribute name, i.e. &lt;xsd:attribute name={name} ... &gt;.
822      * @param viaType viaType, can be used for recursion with or without an intermediate child layer.
823      * @return whether the given node defines an element to have a specified attribute.
824      */
825     private boolean hasElementAttribute(final Node node, final String name, final String viaType)
826     {
827         Node via = viaType == null ? node : DocumentReader.getChild(node, viaType).get();
828         for (int childIndex = 0; childIndex < via.getChildNodes().getLength(); childIndex++)
829         {
830             Node child = via.getChildNodes().item(childIndex);
831             String childName = DocumentReader.getAttribute(child, "name").orElse(null);
832             String childNodeName = child.getNodeName();
833             if (childNodeName.equals("xsd:attribute") && name.equals(childName)
834                     || childNodeName.equals("xsd:sequence") && hasElementAttribute(child, name, null))
835             {
836                 return true;
837             }
838             if (childNodeName.equals("xsd:complexContent") || childNodeName.equals("xsd:simpleContent"))
839             {
840                 Optional<Node> extension = DocumentReader.getChild(child, "xsd:extension");
841                 if (extension.isPresent())
842                 {
843                     Optional<String> base = DocumentReader.getAttribute(extension.get(), "base");
844                     if (base.isPresent())
845                     {
846                         Optional<Node> baseNode = getType(base.get());
847                         // null input, referred types are already complex
848                         if (baseNode.isPresent() && hasElementAttribute(baseNode.get(), name, null))
849                         {
850                             return true;
851                         }
852                     }
853                     if (hasElementAttribute(extension.get(), name, null)) // null, xsd:extension directly contains xsd:attribute
854                     {
855                         return true;
856                     }
857                 }
858             }
859         }
860         return false;
861     }
862 
863     /**
864      * Get the root node.
865      * @return root node.
866      */
867     public Node getRoot()
868     {
869         return this.root;
870     }
871 
872     /**
873      * Returns the node for the given path.
874      * @param path path.
875      * @return type, empty if not known
876      */
877     public Optional<Node> getElement(final String path)
878     {
879         return Optional.ofNullable(this.elements.get(path.replace("ots:", "")));
880     }
881 
882     /**
883      * Returns the type, as pointed to by base={base}.
884      * @param base type.
885      * @return type, as pointed to by base={base}, empty if not known.
886      */
887     public Optional<Node> getType(final String base)
888     {
889         return Optional.ofNullable(this.types.get(base.replace("ots:", "")));
890     }
891 
892     /**
893      * Returns the xsd:key and the paths where they are defined.
894      * @return xsd:key and the paths where they are defined.
895      */
896     public Map<String, Node> keys()
897     {
898         return new LinkedHashMap<>(this.keys);
899     }
900 
901     /**
902      * Returns the xsd:keyref and the paths where they are defined.
903      * @return xsd:keyref and the paths where they are defined.
904      */
905     public Map<String, Node> keyrefs()
906     {
907         return new LinkedHashMap<>(this.keyrefs);
908     }
909 
910     /**
911      * Returns the xsd:unique and the paths where they are defined.
912      * @return xsd:unique and the paths where they are defined.
913      */
914     public Map<String, Node> uniques()
915     {
916         return new LinkedHashMap<>(this.uniques);
917     }
918 
919     /**
920      * Return whether the given node is of the type.
921      * @param node node.
922      * @param path path of the type in dotted xpath notation, e.g. "SignalGroup.TrafficLight".
923      * @return whether the given node is of the type.
924      */
925     public boolean isType(final Node node, final String path)
926     {
927         Optional<String> name = DocumentReader.getAttribute(node, "name");
928         if (name.isPresent() && path.equals(name.get()))
929         {
930             return true;
931         }
932         Node nodeUse = node;
933         if (nodeUse.getNodeName().equals("xsd:element"))
934         {
935             nodeUse = DocumentReader.getChild(node, "xsd:complexType").orElse(null);
936             if (nodeUse == null)
937             {
938                 nodeUse = DocumentReader.getChild(node, "xsd:simpleType").orElse(null);
939                 if (nodeUse == null)
940                 {
941                     return false;
942                 }
943             }
944         }
945         for (int childIndex = 0; childIndex < nodeUse.getChildNodes().getLength(); childIndex++)
946         {
947             Node child = nodeUse.getChildNodes().item(childIndex);
948             if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
949             {
950                 String base = null;
951                 Optional<Node> extension = DocumentReader.getChild(child, "xsd:extension");
952                 if (extension.isPresent())
953                 {
954                     base = DocumentReader.getAttribute(extension.get(), "base").orElse(null);
955                 }
956                 Optional<Node> restriction = DocumentReader.getChild(child, "xsd:restriction");
957                 if (restriction.isPresent())
958                 {
959                     base = DocumentReader.getAttribute(restriction.get(), "base").orElse(null);
960                 }
961                 if (base == null)
962                 {
963                     return false;
964                 }
965                 if (base.endsWith(path))
966                 {
967                     return true;
968                 }
969                 if (!base.startsWith("xsd:"))
970                 {
971                     Optional<Node> baseNode = getType(base);
972                     if (baseNode.isPresent() && !baseNode.equals(nodeUse))
973                     {
974                         return isType(baseNode.get(), path);
975                     }
976                 }
977             }
978         }
979         return false;
980     }
981 
982 }