View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.util.ArrayList;
6   import java.util.Collections;
7   import java.util.LinkedHashMap;
8   import java.util.LinkedHashSet;
9   import java.util.List;
10  import java.util.Map;
11  import java.util.Map.Entry;
12  import java.util.NoSuchElementException;
13  import java.util.Objects;
14  import java.util.Optional;
15  import java.util.Set;
16  import java.util.SortedMap;
17  import java.util.SortedSet;
18  import java.util.TreeMap;
19  import java.util.TreeSet;
20  import java.util.function.Consumer;
21  import java.util.function.Function;
22  import java.util.stream.Collectors;
23  
24  import javax.xml.parsers.ParserConfigurationException;
25  
26  import org.djutils.event.Event;
27  import org.djutils.event.EventListener;
28  import org.djutils.event.EventListenerMap;
29  import org.djutils.event.EventType;
30  import org.djutils.event.LocalEventProducer;
31  import org.djutils.event.reference.Reference;
32  import org.djutils.event.reference.ReferenceType;
33  import org.djutils.exceptions.Throw;
34  import org.djutils.immutablecollections.ImmutableArrayList;
35  import org.djutils.immutablecollections.ImmutableList;
36  import org.djutils.metadata.MetaData;
37  import org.djutils.metadata.ObjectDescriptor;
38  import org.opentrafficsim.base.logger.Logger;
39  import org.opentrafficsim.editor.DocumentReader.NodeAnnotation;
40  import org.opentrafficsim.editor.XsdTreeNodeUtil.LoadingIndices;
41  import org.opentrafficsim.editor.XsdTreeNodeUtil.Occurs;
42  import org.opentrafficsim.editor.decoration.validation.CoupledValidator;
43  import org.opentrafficsim.editor.decoration.validation.KeyValidator;
44  import org.opentrafficsim.editor.decoration.validation.KeyrefValidator;
45  import org.opentrafficsim.editor.decoration.validation.ValueValidator;
46  import org.opentrafficsim.swing.gui.OtsAnimationPanel;
47  import org.w3c.dom.Document;
48  import org.w3c.dom.Element;
49  import org.w3c.dom.Node;
50  import org.w3c.dom.NodeList;
51  import org.xml.sax.SAXException;
52  
53  /**
54   * Underlying data structure object of the editor. Starting with the root node "Ots", all the information is stored in a tree.
55   * The tree follows the XSD logic, e.g. "Ots.Network.Link". {@code XsdTreeNode}'s have a {@code Node} object from the XSD DOM
56   * tree. From this information it can be derived what the child nodes should be, and which attributes are contained.<br>
57   * <br>
58   * This class is mostly straightforward in the sense that there are direct parent-child relations, and that changing an option
59   * replaces a node. When an xsd:sequence is part of an xsd:choice or xsd:all, things become complex as the xsd:sequence is a
60   * single option. Therefore the xsd:sequence becomes a node visible in the tree, when it's an option under a choice.
61   * Furthermore, for each xsd:choice or xsd:all node an {@code XsdTreeNode} is created that is not visible in the tree. It stores
62   * all options {@code XsdTreeNode}'s and knows what option is selected. Only one option is ever in the list of children of the
63   * parent node.
64   * <p>
65   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
66   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
67   * </p>
68   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
69   */
70  public class XsdTreeNode extends LocalEventProducer
71  {
72  
73      /** Event when a node value is changed. */
74      public static final EventType VALUE_CHANGED = new EventType("VALUECHANGED",
75              new MetaData("Value changed", "Value changed on node",
76                      new ObjectDescriptor("Node", "Node with changed value", XsdTreeNode.class),
77                      new ObjectDescriptor("Previous", "Previous node value", String.class)));
78  
79      /** Event when an attribute value is changed. */
80      public static final EventType ATTRIBUTE_CHANGED = new EventType("ATTRIBUTECHANGED",
81              new MetaData("Attribute changed", "Attribute changed on node",
82                      new ObjectDescriptor("Node", "Node with changed attribute value", XsdTreeNode.class),
83                      new ObjectDescriptor("Attribute", "Name of the attribute", String.class),
84                      new ObjectDescriptor("Previous", "Previous attribute value", String.class)));
85  
86      /** Event when an option is changed. */
87      public static final EventType OPTION_CHANGED = new EventType("OPTIONCHANGED",
88              new MetaData("Option changed", "Option changed on node",
89                      new ObjectDescriptor("Node", "Node on which the event is called", XsdTreeNode.class),
90                      new ObjectDescriptor("Selected", "Newly selected option node", XsdTreeNode.class),
91                      new ObjectDescriptor("Previous", "Previously selected option node", XsdTreeNode.class)));
92  
93      /** Event when an option is changed. */
94      public static final EventType ACTIVATION_CHANGED = new EventType("ACTIVATIONCHANGED",
95              new MetaData("Activation changed", "Activation changed on node",
96                      new ObjectDescriptor("Node", "Node with changed activation.", XsdTreeNode.class),
97                      new ObjectDescriptor("Activation", "New activation state.", Boolean.class)));
98  
99      /** Event when a node is moved. */
100     public static final EventType MOVED = new EventType("MOVED",
101             new MetaData("Node moved", "Node moved", new ObjectDescriptor("Node", "Node that was moved.", XsdTreeNode.class),
102                     new ObjectDescriptor("OldIndex", "Old index.", Integer.class),
103                     new ObjectDescriptor("NewIndex", "New index.", Integer.class)));
104 
105     /** Limit on displayed option name to avoid huge menu's. */
106     private static final int MAX_OPTIONNAME_LENGTH = 64;
107 
108     /** Parent node. */
109     @SuppressWarnings("checkstyle:visibilitymodifier")
110     XsdTreeNode parent;
111 
112     /** Node from XSD that this {@code XsdTreeNode} represents. Most typically an xsd:element node. */
113     @SuppressWarnings("checkstyle:visibilitymodifier")
114     Node xsdNode;
115 
116     /** Nodes from XSD that are between the XSD node of the parent, and this tree node's XSD node. */
117     private final ImmutableList<Node> hiddenNodes;
118 
119     /**
120      * Element defining node that referred to a type. The type is defined by {@code xsdNode}, the referring node is used for
121      * original information on name and occurrence. For simple element nodes this is {@code null}.
122      */
123     @SuppressWarnings("checkstyle:visibilitymodifier")
124     Node referringXsdNode;
125 
126     /** XSD schema from which to get type and element nodes that are referred to. */
127     private final Schema schema;
128 
129     /** Minimum number of this element under the parent node, as defined in minOccurs in XSD. */
130     private int minOccurs = 0;
131 
132     /** Maximum number of this element under the parent node, as defined in maxOccurs in XSD. */
133     private int maxOccurs = -1;
134 
135     /**
136      * Path string of this element, e.g. "Ots.Definitions.RoadLayouts". This is used to identify each unique type of element.
137      */
138     private final String pathString;
139 
140     // ====== Choice/Options ======
141 
142     /** Choice node, represents an xsd:choice of which 1 option is shown. All options are {@code XsdTreeNode}'s themselves. */
143     @SuppressWarnings("checkstyle:visibilitymodifier")
144     XsdTreeNode choice;
145 
146     /** Option nodes. These can be directly applicable in the tree, or they can represent an xsd:sequence. */
147     @SuppressWarnings("checkstyle:visibilitymodifier")
148     List<XsdTreeNode> options;
149 
150     /** Currently selected option in the choice node. */
151     @SuppressWarnings("checkstyle:visibilitymodifier")
152     XsdTreeNode selected;
153 
154     // ====== Children ======
155 
156     /** Children nodes. */
157     @SuppressWarnings("checkstyle:visibilitymodifier")
158     List<XsdTreeNode> children;
159 
160     // ====== Attributes ======
161 
162     /** Attribute XSD nodes. */
163     private List<Node> attributeNodes;
164 
165     /** Attribute values. */
166     private List<String> attributeValues;
167 
168     // ====== Properties to expose to the GUI ======
169 
170     /** Whether the node is active. Inactive nodes show the user what type of node can be created in its place. */
171     @SuppressWarnings("checkstyle:visibilitymodifier")
172     boolean active;
173 
174     /**
175      * When the node has been deactivated, activation should only set {@code active = true}. Other parts of the activation
176      * should be ignored as the node was in an active state before, i.e. those parts are in tact. Deactivation does not affect
177      * those parts.
178      */
179     private boolean deactivated;
180 
181     /** Whether this node is identifiable, i.e. has an Id attribute. */
182     private Boolean isIdentifiable;
183 
184     /** Attribute index of Id. */
185     private int idIndex;
186 
187     /** Whether this node is editable, i.e. has a simple value, e.g. &lt;Node&gt;Simple value&lt;/Node&gt;. */
188     private Boolean isEditable;
189 
190     /** Stored simple value of the node. */
191     private String value;
192 
193     /**
194      * Whether this node is loaded from an include file, and hence should not be editable at all. (Not whether this is the
195      * Include node itself.
196      */
197     private boolean isIncluded;
198 
199     // ====== Interaction with visualization ======
200 
201     /** This function can be set externally and supplies an additional {@code String} to clarify this node in the tree. */
202     private Function<XsdTreeNode, String> stringFunction;
203 
204     /** A consumer can be set externally and will receive this node when its pop menu item is selected. */
205     private Map<String, Consumer<XsdTreeNode>> consumers = new LinkedHashMap<>();
206 
207     /** The description, may be {@code null}. */
208     private String description;
209 
210     /** Validators for the node itself, e.g. a check on not being a duplicate node within the parent. */
211     private Set<Function<XsdTreeNode, String>> nodeValidators = new LinkedHashSet<>();
212 
213     /** Validators for the value, CoupledValidators sorted first to couple, after which other validators may invalidate. */
214     private SortedMap<ValueValidator, Object> valueValidators = new TreeMap<>();
215 
216     /** Validators for each attribute, CoupledValidators sorted first to couple, after which other validators may invalidate. */
217     private Map<String, SortedSet<ValueValidator>> attributeValidators = new LinkedHashMap<>();
218 
219     /**
220      * Field objects for each value validator and the attribute it validates. These field objects help the validator find the
221      * correct node value, attribute value or child value from a node. Note that names alone are insufficient as names can be
222      * shared between these.
223      */
224     private Map<String, Map<ValueValidator, Object>> attributeValidatorFields = new LinkedHashMap<>();
225 
226     /** Stored valid status, excluding children. {@code null} means unknown and that it needs to be derived. */
227     private Boolean isSelfValid = null;
228 
229     /** Stored valid status, including children. {@code null} means unknown and that it needs to be derived. */
230     private Boolean isValid = null;
231 
232     /** Stored value valid status. {@code null} means unknown and that it needs to be derived. */
233     private Boolean valueValid = null;
234 
235     /** Value invalid message. */
236     private String valueInvalidMessage = null;
237 
238     /** Stored node valid status. {@code null} means unknown and that it needs to be derived. */
239     private Boolean nodeValid = null;
240 
241     /** Node invalid message (applies only to node itself, e.g. no duplicate nodes in parent). */
242     private String nodeInvalidMessage = null;
243 
244     /** Stored attribute valid status. {@code null} means unknown and that it needs to be derived. */
245     private List<Boolean> attributeValid;
246 
247     /** Attribute invalid message. */
248     private List<String> attributeInvalidMessage;
249 
250     /**
251      * Constructor for root node, based on an {@code XsdSchema}. Note: {@code XsdTreeNodeRoot} should be used for the root. The
252      * {@code XsdSchema} will be available to all nodes in the tree.
253      * @param schema XSD schema.
254      */
255     protected XsdTreeNode(final Schema schema)
256     {
257         Throw.whenNull(schema, "XsdSchema may not be null.");
258         this.parent = null;
259         this.hiddenNodes = new ImmutableArrayList<>(Collections.emptyList());
260         this.schema = schema;
261         this.xsdNode = this.schema.getRoot();
262         this.referringXsdNode = null;
263         setOccurs();
264         this.pathString = buildPathLocation();
265         this.active = true;
266         this.isIncluded = false;
267     }
268 
269     /**
270      * Construct a node without referring node.
271      * @param parent parent.
272      * @param xsdNode XSD node that this tree node represents.
273      * @param hiddenNodes nodes between the XSD node of the parent, and this tree node's XSD node.
274      */
275     XsdTreeNode(final XsdTreeNode parent, final Node xsdNode, final ImmutableList<Node> hiddenNodes)
276     {
277         this(parent, xsdNode, hiddenNodes, null);
278     }
279 
280     /**
281      * Constructor with referring node for extended types. If the node is xsd:choice or xsd:all, this node will represent the
282      * choice. If the node is xsd:sequence, this node will represent the sequence.<br>
283      * <br>
284      * The hidden nodes are all elements between the parent element and this element. For example {xsd:complexType, xsd:choice}
285      * between the following child element and its containing object:
286      *
287      * <pre>
288      * &lt;xsd:element name="OBJECT"&gt;
289      *   &lt;xsd:complexType&gt;
290      *     &lt;xsd:choice&gt;
291      *       &lt;xsd:element name="CHILD" /&gt;
292      *     &lt;/xsd:choice&gt;
293      *   &lt;/xsd:complexType&gt;
294      * &lt;/xsd:element&gt;
295      * </pre>
296      *
297      * The hidden nodes will not include a referring node. For example the following "OBJECT" element will result in hidden
298      * nodes {xsd:complexType, xsd:sequence} and the referring node is the {@code Node} with the ref="OBJECT" attribute. The
299      * {@code XsdTreeNode} representing this element will itself wrap the referred node with name="OBJECT" as shown above.
300      *
301      * <pre>
302      * &lt;xsd:element name="PARENT"&gt;
303      *   &lt;xsd:complexType&gt;
304      *     &lt;xsd:sequence&gt;
305      *       &lt;xsd:element ref="OBJECT" /&gt;
306      *     &lt;/xsd:sequence&gt;
307      *   &lt;/xsd:complexType&gt;
308      * &lt;/xsd:element&gt;
309      * </pre>
310      *
311      * @param parent parent.
312      * @param xsdNode XSD node that this tree node represents.
313      * @param hiddenNodes nodes between the XSD node of the parent and this tree node's XSD node.
314      * @param referringXsdNode original node that referred to {@code Node} through a ref={ref} or type={type} attribute, it is
315      *            used for naming and occurrence, may be {@code null} if not applicable.
316      */
317     XsdTreeNode(final XsdTreeNode parent, final Node xsdNode, final ImmutableList<Node> hiddenNodes,
318             final Node referringXsdNode)
319     {
320         Throw.whenNull(xsdNode, "Node may not be null.");
321         this.parent = parent;
322         this.xsdNode = xsdNode;
323         this.hiddenNodes = hiddenNodes;
324         this.referringXsdNode = referringXsdNode;
325         this.schema = parent.schema;
326         setOccurs();
327         this.active = this.minOccurs > 0;
328         this.pathString = buildPathLocation();
329         this.isIncluded = parent.isIncluded;
330         this.value = referringXsdNode == null ? DocumentReader.getAttribute(xsdNode, "default").orElse(null)
331                 : DocumentReader.getAttribute(referringXsdNode, "default").orElse(null);
332     }
333 
334     /**
335      * Sets the minOccurs and maxOccurs values based on the relevant XSD node and hidden nodes. Note that this does not comply
336      * to the full XSD logic. Here, the product of minOccurs and maxOccurs is derived. For OBJECT as below this results in
337      * minOccurs = 1x4 = 4 and maxOccurs = 2x5 = 10. The complete logic is that any specific combination is allowed, i.e 1x4,
338      * 1x5, 2x4, 2x5 and {1x4 + 1x5}, i.e. 4, 5, 8, 9 and 10. We ignore this, as things can become highly complex when multiple
339      * choices and sequences are sequenced in a parent choice or sequence.
340      *
341      * <pre>
342      * &lt;xsd:sequence minOccurs="1" maxOccurs="2"&gt;
343      *   &lt;xsd:sequence minOccurs="4" maxOccurs="5"&gt;
344      *     &lt;xsd:element name="OBJECT"&gt;
345      *   &lt;/xsd:sequence&gt;
346      * &lt;/xsd:sequence&gt;
347      * </pre>
348      */
349     private void setOccurs()
350     {
351         Node node = this.choice != null ? this.choice.xsdNode : getRelevantNode();
352         this.minOccurs = Occurs.MIN.get(node);
353         this.maxOccurs = Occurs.MAX.get(node);
354         // An xsd:all may occur as often as the number of child elements. Presented as a choice, each element may appear once,
355         // or not at all (we assume maxOccurs=1 on the child elements). Uniqueness is automatically checked elsewhere through
356         // XsdAllValidator. For example we could have 1 up to N lane change incentives, all unique, but never more than N.
357         if (getNodeName().equals("xsd:all"))
358         {
359             int childCount = 0;
360             for (int i = 0; i < this.xsdNode.getChildNodes().getLength(); i++)
361             {
362                 Node child = this.xsdNode.getChildNodes().item(i);
363                 if (!child.getNodeName().equals("#text"))
364                 {
365                     childCount++;
366                 }
367             }
368             this.maxOccurs *= childCount;
369         }
370     }
371 
372     /**
373      * Builds the path location, e.g. "Ots.Definitions.RoadLayouts".
374      * @return the path location.
375      */
376     private String buildPathLocation()
377     {
378         List<XsdTreeNode> path = getPath();
379         StringBuilder pathStr = new StringBuilder(((XsdTreeNode) path.get(0)).getNodeName());
380         for (int i = 1; i < path.size(); i++)
381         {
382             String nodeString = ((XsdTreeNode) path.get(i)).getNodeName();
383             // skip these intermediate node types in the path string of the sub-tree, only include if leaf path is requested
384             if ((!nodeString.equals("xsd:choice") && !nodeString.equals("xsd:all") && !nodeString.equals("xsd:sequence")
385                     && !nodeString.equals("xi:include")) || i == path.size() - 1)
386             {
387                 pathStr.append(".").append(nodeString);
388             }
389         }
390         return pathStr.toString();
391     }
392 
393     /**
394      * Returns the path from root to this node.
395      * @return list of node along the path.
396      */
397     public List<XsdTreeNode> getPath()
398     {
399         List<XsdTreeNode> path = this.parent != null ? this.parent.getPath() : new ArrayList<>();
400         path.add(this);
401         return path;
402     }
403 
404     /**
405      * Returns the root node.
406      * @return root node.
407      */
408     public XsdTreeNodeRoot getRoot()
409     {
410         return this.parent.getRoot();
411     }
412 
413     /**
414      * Returns the relevant node describing this element. This is {@code this.referringXsdNode} if there is one,
415      * {@code this.xsdNode} otherwise.
416      * @return the relevant node describing this element
417      */
418     private Node getRelevantNode()
419     {
420         return this.referringXsdNode == null ? this.xsdNode : this.referringXsdNode;
421     }
422 
423     /**
424      * Returns whether this is a virtual layer node representing a sequence.
425      * @return whether this is a virtual layer node representing a sequence
426      */
427     public boolean isSequence()
428     {
429         return this.xsdNode.getNodeName().equals("xsd:sequence");
430     }
431 
432     // ====== Choice/Options ======
433 
434     /**
435      * Returns whether this node is (part of) a choice, i.e. should show an option can be set here.
436      * @return whether this node is (part of) a choice, i.e. should show an option can be set here.
437      */
438     public boolean isChoice()
439     {
440         return this.choice != null;
441     }
442 
443     /**
444      * Returns a list of options including current selection status.
445      * @return list of options including current selection status.
446      */
447     public List<XsdOption> getOptions()
448     {
449         List<XsdOption> out = new ArrayList<>();
450         if (this.choice != null)
451         {
452             for (XsdTreeNode node : this.choice.options)
453             {
454                 out.add(new XsdOption(node, this.choice, node.equals(this.choice.selected)));
455             }
456         }
457         return out;
458     }
459 
460     /**
461      * Sets the node as newly selected option. All current nodes from the choice set are removed from the parent node.
462      * @param node newly selected node. Must be part of the choice that this node represents.
463      */
464     public void setOption(final XsdTreeNode node)
465     {
466         Throw.when(!isChoice(), IllegalStateException.class, "Setting option on node that is not (part of) a choice.");
467         Throw.when(!this.choice.options.contains(node) && !this.choice.equals(node), IllegalStateException.class,
468                 "Setting option on node that does not have this option.");
469         XsdTreeNode previous = this.choice.selected == null ? this.choice : this.choice.selected;
470         if (node.equals(previous))
471         {
472             return;
473         }
474         this.choice.selected = node;
475         int index = removeOptionFromParent();
476         this.parent.children.add(index, node);
477         node.invalidate();
478         this.choice.options.forEach((n) -> n.fireEvent(XsdTreeNodeRoot.OPTION_CHANGED, new Object[] {n, node, previous}));
479     }
480 
481     /**
482      * Removes option from the parent and return the appropriate index to insert newly chosen option.
483      * @return insertion index for new option.
484      */
485     private int removeOptionFromParent()
486     {
487         int removeIndex = this.parent.children.indexOf(this);
488         if (removeIndex >= 0)
489         {
490             this.parent.children.remove(removeIndex);
491             return removeIndex;
492         }
493         for (XsdTreeNode node : this.choice.options)
494         {
495             removeIndex = this.parent.children.indexOf(node);
496             if (removeIndex >= 0)
497             {
498                 this.parent.children.remove(removeIndex);
499                 return removeIndex;
500             }
501         }
502         return this.parent.children.size();
503     }
504 
505     /**
506      * Returns the selected option.
507      * @return selected option.
508      */
509     public XsdTreeNode getOption()
510     {
511         return this.choice.selected;
512     }
513 
514     /**
515      * Creates the option nodes as part of an xsd:choice or xsd:all node.
516      */
517     void createOptions()
518     {
519         Throw.when(!this.xsdNode.getNodeName().equals("xsd:choice") && !this.xsdNode.getNodeName().equals("xsd:all"),
520                 IllegalStateException.class, "Can only add options for a node of type xsd:choice or xsd:all.");
521         this.options = new ArrayList<>();
522         XsdTreeNodeUtil.addChildren(this.xsdNode, this.parent, this.options, this.hiddenNodes, this.schema, false, -1);
523         this.choice = this;
524         for (XsdTreeNode option : this.options)
525         {
526             option.minOccurs = this.minOccurs;
527             option.maxOccurs = this.maxOccurs;
528             if (this.minOccurs == 0)
529             {
530                 option.active = false;
531             }
532             option.choice = this;
533         }
534         if (this.choice.xsdNode.getNodeName().equals("xsd:all"))
535         {
536             for (XsdTreeNode option : this.options)
537             {
538                 XsdTreeNodeUtil.addXsdAllValidator(this.choice, option);
539             }
540         }
541     }
542 
543     /**
544      * Returns whether this node only contains a single choice and nothing else. This can be used to shortcut information shown
545      * to a user, e.g. showing the selected child option in the presented name of this node.
546      * @return whether this node only contains a single choice and nothing else
547      */
548     public boolean isSingleChoiceType()
549     {
550         boolean choiceFound = false;
551         for (int i = 0; i < this.xsdNode.getChildNodes().getLength(); i++)
552         {
553             Node child = this.xsdNode.getChildNodes().item(i);
554             String name = child.getNodeName();
555             if (name.equals("xsd:choice") && Occurs.MAX.get(child) == 1)
556             {
557                 if (choiceFound)
558                 {
559                     return false;
560                 }
561                 choiceFound = true;
562             }
563             else if (!name.equals("#text"))
564             {
565                 return false;
566             }
567         }
568         return choiceFound;
569     }
570 
571     // ====== Children ======
572 
573     /**
574      * Returns the number of children.
575      * @return number of children.
576      */
577     public int getChildCount()
578     {
579         if (!this.active)
580         {
581             return 0;
582         }
583         assureChildren();
584         return this.children.size();
585     }
586 
587     /**
588      * Sets the given node as child of this node.
589      * @param index index to insert the node.
590      * @param child child node.
591      */
592     public void setChild(final int index, final XsdTreeNode child)
593     {
594         if (index >= this.children.size())
595         {
596             this.children.add(child);
597         }
598         else
599         {
600             this.children.add(index, child);
601         }
602         child.parent = this;
603         child.invalidate();
604     }
605 
606     /**
607      * Returns the child at given index.
608      * @param index child index.
609      * @return child.
610      */
611     public XsdTreeNode getChild(final int index)
612     {
613         assureChildren();
614         return this.children.get(index);
615     }
616 
617     /**
618      * Returns whether this node has a child with given name.
619      * @param name child name
620      * @return whether this node has a child with given name
621      */
622     public boolean hasChild(final String name)
623     {
624         assureChildren();
625         for (XsdTreeNode child : this.children)
626         {
627             if (child.getNodeName().equals("xsd:sequence") || child.getNodeName().equals("xsd:choice")
628                     || child.getNodeName().equals("xsd:all"))
629             {
630                 return child.hasChild(name);
631             }
632             if (child.getNodeName().equals(name))
633             {
634                 return true;
635             }
636         }
637         return false;
638     }
639 
640     /**
641      * Returns the first child with given name. The node may be within a series of xsd:sequence and xsd:choice/xsd:all
642      * intermediate nodes.
643      * @param name child name.
644      * @return child.
645      * @throws NoSuchElementException when no child with given name exists
646      */
647     public XsdTreeNode getFirstChild(final String name)
648     {
649         assureChildren();
650         for (XsdTreeNode child : this.children)
651         {
652             if (child.getNodeName().equals("xsd:sequence") || child.getNodeName().equals("xsd:choice")
653                     || child.getNodeName().equals("xsd:all"))
654             {
655                 try
656                 {
657                     return child.getFirstChild(name);
658                 }
659                 catch (NoSuchElementException ex)
660                 {
661                     // continue search at other children
662                 }
663             }
664             if (child.getNodeName().equals(name))
665             {
666                 return child;
667             }
668         }
669         throw new NoSuchElementException("Node does not have a child named " + name);
670     }
671 
672     /**
673      * Returns a list of the child nodes.
674      * @return list of the child nodes; safe copy.
675      */
676     public List<XsdTreeNode> getChildren()
677     {
678         assureChildren();
679         return new ArrayList<>(this.children);
680     }
681 
682     /**
683      * Assures children are present. If a child has minOccurs &gt; 1, additional child nodes are added. Result is cached.
684      */
685     protected void assureChildren()
686     {
687         if (this.children != null)
688         {
689             return;
690         }
691         if (!this.active)
692         {
693             return;
694         }
695         this.children = new ArrayList<>();
696         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
697         {
698             if (this.attributeValues == null || this.attributeValues.get(0) == null)
699             {
700                 return;
701             }
702             File file = new File(this.attributeValues.get(0));
703             if (!file.isAbsolute())
704             {
705                 file = new File(getRoot().getDirectory() + this.attributeValues.get(0));
706             }
707             if (!file.exists() && this.attributeValues.get(1) != null)
708             {
709                 file = new File(this.attributeValues.get(1));
710                 if (!file.isAbsolute())
711                 {
712                     file = new File(getRoot().getDirectory() + this.attributeValues.get(1));
713                 }
714             }
715             if (file.exists())
716             {
717                 Document document;
718                 try
719                 {
720                     document = DocumentReader.open(file.toURI());
721                 }
722                 catch (SAXException | IOException | ParserConfigurationException exception)
723                 {
724                     return;
725                 }
726                 Node xsdIncludeNode = document.getFirstChild();
727                 String nameXml = xsdIncludeNode.getNodeName().replace("ots:", "");
728                 // Load the include file in to a node with semantics equal to a sibling with the same name. E.g. an include with
729                 // a main <ots:GtuTypes> tag can be loaded by an include next to a GtuTypes node.
730                 for (XsdTreeNode sibling : this.parent.children)
731                 {
732                     if (sibling.isRelevantNode(nameXml))
733                     {
734                         XsdTreeNode child =
735                                 new XsdTreeNode(this, sibling.xsdNode, sibling.hiddenNodes, sibling.referringXsdNode);
736                         child.isIncluded = true;
737                         this.children.add(child);
738                         getRoot().fireEvent(XsdTreeNodeRoot.NODE_CREATED,
739                                 new Object[] {child, child.parent, child.parent.children.indexOf(child)});
740                         child.loadXmlNodes(xsdIncludeNode);
741                         return;
742                     }
743                 }
744             }
745         }
746         else if (!this.xsdNode.hasChildNodes())
747         {
748             return;
749         }
750         Map<Node, ImmutableList<Node>> relevantNodes = XsdTreeNodeUtil.getRelevantNodesWithChildren(this.xsdNode,
751                 new ImmutableArrayList<>(Collections.emptyList()), this.schema);
752         for (Entry<Node, ImmutableList<Node>> entry : relevantNodes.entrySet())
753         {
754             XsdTreeNodeUtil.addChildren(entry.getKey(), this, this.children, entry.getValue(), this.schema, true, -1);
755         }
756         for (int index = 0; index < this.children.size(); index++)
757         {
758             XsdTreeNode child = this.children.get(index);
759             for (int occurs = 1; occurs < child.minOccurs; occurs++)
760             {
761                 if (!child.isActive())
762                 {
763                     child.setActive();
764                 }
765                 child.add();
766                 index++;
767             }
768         }
769     }
770 
771     /**
772      * Returns the parent node.
773      * @return parent node, is {@code null} for the root.
774      */
775     public XsdTreeNode getParent()
776     {
777         return this.parent;
778     }
779 
780     // ====== Attributes ======
781 
782     /**
783      * Finds all attributes that meet the following structure. Also finds the most specific description found in the type
784      * inheritance for attributes along the way.
785      *
786      * <pre>
787      * &lt;xsd:element ...&gt;
788      *   ...
789      *   &lt;xsd:complexType&gt;
790      *     ...
791      *     &lt;xsd:attribute name="NAME" use="USE" ...&gt;
792      *     ...
793      *   &lt;/xsd:complexType&gt;
794      *   ...
795      * &lt;/xsd:element&gt;
796      * </pre>
797      */
798     private synchronized void assureAttributesAndDescription()
799     {
800         if (this.attributeNodes != null)
801         {
802             return;
803         }
804         this.attributeNodes = new ArrayList<>();
805         this.attributeValues = new ArrayList<>();
806         this.attributeValid = new ArrayList<>();
807         this.attributeInvalidMessage = new ArrayList<>();
808         this.description = NodeAnnotation.DESCRIPTION.get(getRelevantNode()).orElse(null);
809         int descriptionSpecificity = this.description != null ? 0 : Integer.MIN_VALUE;
810         Node complexType =
811                 (this.xsdNode.getNodeName().equals("xsd:complexType") || this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
812                         ? this.xsdNode : DocumentReader.getChild(this.xsdNode, "xsd:complexType").orElse(null);
813         if (complexType != null && this.xsdNode.hasChildNodes())
814         {
815             findAttributesAndDescription(complexType, -1, descriptionSpecificity);
816         }
817     }
818 
819     /**
820      * Finds attributes in a recursive way, looking up base types. Also finds the most specific node description. Multiple
821      * descriptions may exist at the same specificity, in which case it is arbitrary which description is used. Note that the
822      * recursion moves to less specific types (e.g. from a type to what a type extends from).
823      * @param node node.
824      * @param nodeSpecificity specificity of node, reflects layers, used to find the most specific description.
825      * @param descriptionSpecificity specificity of current description
826      */
827     private void findAttributesAndDescription(final Node node, final int nodeSpecificity, final int descriptionSpecificity)
828     {
829         Optional<String> descript = NodeAnnotation.DESCRIPTION.get(node);
830         int updatedDescriptionSpecificity = descriptionSpecificity;
831         if (descript.isPresent() && descriptionSpecificity < nodeSpecificity)
832         {
833             updatedDescriptionSpecificity = nodeSpecificity;
834             this.description = descript.get();
835         }
836         for (int childIndex = 0; childIndex < node.getChildNodes().getLength(); childIndex++)
837         {
838             Node child = node.getChildNodes().item(childIndex);
839             if (child.getNodeName().equals("xsd:attribute") && DocumentReader.getAttribute(child, "name").isPresent())
840             {
841                 this.attributeNodes.add(child);
842                 this.attributeValues.add(null);
843                 this.attributeValid.add(null);
844                 this.attributeInvalidMessage.add(null);
845             }
846             if (child.getNodeName().equals("xsd:complexContent") || child.getNodeName().equals("xsd:simpleContent"))
847             {
848                 Optional<Node> extension = DocumentReader.getChild(child, "xsd:extension");
849                 if (extension.isPresent())
850                 {
851                     findAttributesAndDescription(extension.get(), nodeSpecificity - 1, updatedDescriptionSpecificity);
852                     String base = DocumentReader.getAttribute(extension.get(), "base").get();
853                     Optional<Node> baseNode = this.schema.getType(base);
854                     if (baseNode.isPresent())
855                     {
856                         findAttributesAndDescription(baseNode.get(), nodeSpecificity - 2, updatedDescriptionSpecificity);
857                     }
858                 }
859                 Optional<Node> restriction = DocumentReader.getChild(child, "xsd:restriction");
860                 if (restriction.isPresent())
861                 {
862                     String base = DocumentReader.getAttribute(restriction.get(), "base").get();
863                     Optional<Node> baseNode = this.schema.getType(base);
864                     if (baseNode.isPresent())
865                     {
866                         findAttributesAndDescription(baseNode.get(), nodeSpecificity - 2, descriptionSpecificity);
867                     }
868                 }
869             }
870         }
871     }
872 
873     /**
874      * Returns the number of attributes.
875      * @return number of attributes.
876      */
877     public int attributeCount()
878     {
879         if (!this.active)
880         {
881             return 0; // show nothing in attribute table for inactive nodes
882         }
883         assureAttributesAndDescription();
884         return this.attributeNodes.size();
885     }
886 
887     /**
888      * Returns the attributes at given index.
889      * @param index attribute index.
890      * @return attribute node.
891      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
892      */
893     public Node getAttributeNode(final int index)
894     {
895         assureAttributesAndDescription();
896         Objects.checkIndex(index, attributeCount());
897         return this.attributeNodes.get(index);
898     }
899 
900     /**
901      * Sets an attribute value.
902      * @param index index of the attribute.
903      * @param value value of the attribute.
904      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
905      */
906     @SuppressWarnings("checkstyle:hiddenfield")
907     public void setAttributeValue(final int index, final String value)
908     {
909         Objects.checkIndex(index, attributeCount());
910         String previous = this.attributeValues.get(index);
911         if (!XsdTreeNodeUtil.valuesAreEqual(previous, value))
912         {
913             boolean isDefaultBoolean = false;
914             if ("xsd:boolean".equals(DocumentReader.getAttribute(this.attributeNodes.get(index), "type").orElse(null)))
915             {
916                 isDefaultBoolean = getDefaultAttributeValue(index).equals(value);
917             }
918             this.attributeValues.set(index, (value == null || value.isEmpty() || isDefaultBoolean) ? null : value);
919             if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
920             {
921                 removeChildren();
922                 this.children = null;
923                 assureChildren();
924             }
925             invalidate();
926             fireEvent(ATTRIBUTE_CHANGED, new Object[] {this, getAttributeNameByIndex(index), previous});
927         }
928     }
929 
930     /**
931      * Returns the default value for the attribute.
932      * @param index attribute index.
933      * @return default value for the attribute, empty if there is no default value.
934      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
935      */
936     public Optional<String> getDefaultAttributeValue(final int index)
937     {
938         assureAttributesAndDescription();
939         Objects.checkIndex(index, attributeCount());
940         return DocumentReader.getAttribute(this.attributeNodes.get(index), "default");
941     }
942 
943     /**
944      * Sets an attribute value.
945      * @param name name of the attribute.
946      * @param value value of the attribute.
947      * @throws NoSuchElementException when the attribute is not in this node.
948      */
949     @SuppressWarnings("checkstyle:hiddenfield")
950     public void setAttributeValue(final String name, final String value)
951     {
952         setAttributeValue(getAttributeIndexByName(name), value);
953     }
954 
955     /**
956      * Returns an attribute value.
957      * @param index index of the attribute.
958      * @return value of the attribute.
959      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
960      */
961     public String getAttributeValue(final int index)
962     {
963         assureAttributesAndDescription();
964         Objects.checkIndex(index, attributeCount());
965         return this.attributeValues.get(index);
966     }
967 
968     /**
969      * Returns an attribute value.
970      * @param attribute name of the attribute.
971      * @return value of the attribute.
972      * @throws NoSuchElementException when the attribute is not in this node.
973      */
974     public String getAttributeValue(final String attribute)
975     {
976         assureAttributesAndDescription();
977         return this.attributeValues.get(getAttributeIndexByName(attribute));
978     }
979 
980     /**
981      * Returns the index of the named attribute.
982      * @param attribute attribute name.
983      * @return index of the named attribute.
984      * @throws NoSuchElementException when the attribute is not in this node.
985      */
986     public int getAttributeIndexByName(final String attribute)
987     {
988         assureAttributesAndDescription();
989         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
990         {
991             switch (attribute)
992             {
993                 case "File":
994                     return 0;
995                 case "Fallback":
996                     return 1;
997                 default:
998                     throw new NoSuchElementException("Attribute " + attribute + " is not in node " + getNodeName() + ".");
999             }
1000         }
1001         for (int index = 0; index < this.attributeNodes.size(); index++)
1002         {
1003             Node attr = this.attributeNodes.get(index);
1004             if (attribute.equals(DocumentReader.getAttribute(attr, "name").orElse(null)))
1005             {
1006                 return index;
1007             }
1008         }
1009         throw new NoSuchElementException("Attribute " + attribute + " is not in node " + getNodeName() + ".");
1010     }
1011 
1012     /**
1013      * Returns the name of the indexed attribute.
1014      * @param index attribute index.
1015      * @return name of the indexed attribute.
1016      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
1017      */
1018     public String getAttributeNameByIndex(final int index)
1019     {
1020         Objects.checkIndex(index, attributeCount());
1021         return DocumentReader.getAttribute(this.attributeNodes.get(index), "name").get();
1022     }
1023 
1024     /**
1025      * Returns whether this node has an attribute with given name.
1026      * @param attribute attribute name.
1027      * @return whether this node has an attribute with given name.
1028      */
1029     public boolean hasAttribute(final String attribute)
1030     {
1031         assureAttributesAndDescription();
1032         for (int index = 0; index < this.attributeNodes.size(); index++)
1033         {
1034             Node attr = this.attributeNodes.get(index);
1035             if (attribute.equals(DocumentReader.getAttribute(attr, "name").orElse(null)))
1036             {
1037                 return true;
1038             }
1039         }
1040         return false;
1041     }
1042 
1043     // ====== GUI interaction ======
1044 
1045     /**
1046      * Returns whether the node is active. If not, it only exists to show the user what type of node may be created here.
1047      * @return whether the node is active.
1048      */
1049     public boolean isActive()
1050     {
1051         return this.active;
1052     }
1053 
1054     /**
1055      * Sets this node to be active if it is not already. If this node is the selected node within a choice, it also activates
1056      * all other options of the choice. For include nodes, or the included child nodes, activation is cascaded down the tree.
1057      */
1058     public void setActive()
1059     {
1060         if (!this.active)
1061         {
1062             this.active = true;
1063             if (this.deactivated)
1064             {
1065                 if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE) || this.isIncluded)
1066                 {
1067                     // included children
1068                     for (XsdTreeNode child : this.children)
1069                     {
1070                         child.setActive();
1071                     }
1072                 }
1073                 invalidate();
1074                 fireEvent(new Event(XsdTreeNodeRoot.ACTIVATION_CHANGED, new Object[] {this, true}));
1075                 return; // deactivated from an active state in the past; all parts below are already in place
1076             }
1077             this.children = null;
1078             this.attributeNodes = null;
1079             this.isIdentifiable = null;
1080             this.isEditable = null;
1081             assureAttributesAndDescription();
1082             assureChildren();
1083             if (this.choice != null && this.choice.selected.equals(this))
1084             {
1085                 this.choice.active = true;
1086                 for (XsdTreeNode option : this.choice.options)
1087                 {
1088                     if (!option.equals(this))
1089                     {
1090                         option.setActive();
1091                     }
1092                 }
1093             }
1094             invalidate();
1095             fireEvent(new Event(XsdTreeNodeRoot.ACTIVATION_CHANGED, new Object[] {this, true}));
1096         }
1097     }
1098 
1099     /**
1100      * Deactivates this node if it is not already. This does not remove child nodes but only sets a boolean toggle. For include
1101      * nodes, or the included child nodes, deactivation is cascaded down the tree.
1102      */
1103     public void setInactive()
1104     {
1105         if (this.active)
1106         {
1107             this.deactivated = true;
1108             this.active = false;
1109             invalidate();
1110             fireEvent(new Event(XsdTreeNode.ACTIVATION_CHANGED, new Object[] {this, false}));
1111             // included children
1112             if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE) || this.isIncluded)
1113             {
1114                 for (XsdTreeNode child : this.children)
1115                 {
1116                     child.setInactive();
1117                 }
1118             }
1119         }
1120     }
1121 
1122     /**
1123      * Returns whether this node has an attribute named "Id". For inactive nodes {@code false} is returned as no id is editable.
1124      * @return whether this node has an attribute named "Id".
1125      */
1126     public boolean isIdentifiable()
1127     {
1128         if (!this.active)
1129         {
1130             return false;
1131         }
1132         if (this.isIdentifiable == null)
1133         {
1134             assureAttributesAndDescription();
1135             for (int index = 0; index < attributeCount(); index++)
1136             {
1137                 Node node = this.attributeNodes.get(index);
1138                 if ("Id".equals(DocumentReader.getAttribute(node, "name").orElse(null)))
1139                 {
1140                     this.isIdentifiable = true;
1141                     this.idIndex = index;
1142                     return true;
1143                 }
1144             }
1145             this.isIdentifiable = false;
1146         }
1147         return this.isIdentifiable;
1148     }
1149 
1150     /**
1151      * Sets the value for an attribute with name "Id".
1152      * @param id value to set.
1153      * @throws NoSuchElementException when the node has no id attribute
1154      */
1155     public void setId(final String id)
1156     {
1157         Throw.when(!isIdentifiable(), NoSuchElementException.class, "Node is non-identifiable.");
1158         setAttributeValue(this.idIndex, id);
1159     }
1160 
1161     /**
1162      * Returns the value of an attribute with name "Id".
1163      * @return value of an attribute with name "Id".
1164      * @throws NoSuchElementException when the node has no id attribute
1165      */
1166     public String getId()
1167     {
1168         Throw.when(!isIdentifiable(), NoSuchElementException.class, "Getting id from non-identifiable node.");
1169         return this.attributeValues.get(this.idIndex);
1170     }
1171 
1172     /**
1173      * Returns whether this node is editable; i.e. whether a value can be set on the node, i.e. has a simple value, e.g.
1174      * &lt;Node&gt;Simple value&lt;/Node&gt;. For inactive nodes {@code false} is returned as no value is editable.
1175      * @return whether this node is editable, i.e. whether a value can be set on the node.
1176      */
1177     public boolean isEditable()
1178     {
1179         if (!this.active)
1180         {
1181             return false;
1182         }
1183         if (this.isEditable == null)
1184         {
1185             this.isEditable = XsdTreeNodeUtil.isEditable(this.xsdNode, this.schema);
1186         }
1187         return this.isEditable;
1188     }
1189 
1190     /**
1191      * Sets the value on this node.
1192      * @param value value to set.
1193      */
1194     public void setValue(final String value)
1195     {
1196         Throw.when(!isEditable(), IllegalStateException.class,
1197                 "Node is not an xsd:simpleType or xsd:complexType with xsd:simpleContent, hence no value is allowed.");
1198         String previous = this.value;
1199         if (!XsdTreeNodeUtil.valuesAreEqual(previous, value))
1200         {
1201             this.value = (value == null || value.isEmpty()) ? null : value;
1202             invalidate();
1203             fireEvent(new Event(VALUE_CHANGED, new Object[] {this, previous}));
1204         }
1205     }
1206 
1207     /**
1208      * Returns the value of this node.
1209      * @return value of this node.
1210      */
1211     public String getValue()
1212     {
1213         return this.value;
1214     }
1215 
1216     /**
1217      * Returns whether this node exists as its loaded from an include.
1218      * @return whether this node exists as its loaded from an include.
1219      */
1220     public boolean isIncluded()
1221     {
1222         return this.isIncluded;
1223     }
1224 
1225     /**
1226      * Returns whether a node of the same type may be added next to this node under the parent. This is based on maxOccurs.
1227      * @return whether a node of the same type may be added next to this node under the parent.
1228      */
1229     public boolean isAddable()
1230     {
1231         return isActive() && (this.maxOccurs == -1 || (this.parent != null && siblingPositions().size() < this.maxOccurs));
1232     }
1233 
1234     /**
1235      * Adds a node of similar type next to this node under the parent. If the node is a choice, the same selected option will be
1236      * set in the added node. In this way the user sees that node option copied.
1237      * @return added node.
1238      */
1239     public XsdTreeNode add()
1240     {
1241         if (this.choice != null)
1242         {
1243             int index = this.parent.children.indexOf(this) + 1;
1244             XsdTreeNode node = new XsdTreeNode(this.choice.parent, this.choice.xsdNode, this.choice.hiddenNodes,
1245                     this.choice.referringXsdNode);
1246             getRoot().fireEvent(XsdTreeNodeRoot.NODE_CREATED,
1247                     new Object[] {node, node.parent, node.parent.children.indexOf(this) + 1});
1248             node.createOptions();
1249             int indexSelected = this.choice.options.indexOf(this.choice.selected);
1250             XsdTreeNode selectedOption = node.options.get(indexSelected);
1251             node.choice.setOption(selectedOption);
1252             this.parent.children.remove(selectedOption); // needs to be at the right index
1253             this.parent.children.add(index, selectedOption);
1254             node.options.get(indexSelected).setActive();
1255             return selectedOption;
1256         }
1257         else
1258         {
1259             int index = this.parent.children.indexOf(this) + 1;
1260             XsdTreeNode node = new XsdTreeNode(this.parent, this.xsdNode, this.hiddenNodes, this.referringXsdNode);
1261             this.parent.children.add(index, node);
1262             node.active = true;
1263             getRoot().fireEvent(XsdTreeNodeRoot.NODE_CREATED,
1264                     new Object[] {node, node.parent, node.parent.children.indexOf(node)});
1265             return node;
1266         }
1267     }
1268 
1269     /**
1270      * Creates a full copy of this node, next to this node under the same parent.
1271      * @return newly created node.
1272      */
1273     public XsdTreeNode duplicate()
1274     {
1275         return duplicate(this.parent);
1276     }
1277 
1278     /**
1279      * Duplicates this node, but under the given parent node.
1280      * @param newParent parent node.
1281      * @return newly created node.
1282      */
1283     public XsdTreeNode duplicate(final XsdTreeNode newParent)
1284     {
1285         // empty copy
1286         XsdTreeNode copyNode = emptyCopy(newParent);
1287         copyNode.active = this.active;
1288         copyInto(copyNode);
1289         copyNode.invalidate();
1290         getRoot().fireEvent(XsdTreeNodeRoot.NODE_CREATED,
1291                 new Object[] {copyNode, newParent, newParent.children.indexOf(copyNode)});
1292         invalidate(); // due to e.g. duplicate ID, this node may also become invalid
1293         return copyNode;
1294     }
1295 
1296     /**
1297      * Creates an empty copy of this node, i.e.&nbsp;without children, options, attributes.
1298      * @return empty copy.
1299      */
1300     public XsdTreeNode emptyCopy()
1301     {
1302         return emptyCopy(this.parent);
1303     }
1304 
1305     /**
1306      * Returns an empty copy of this node under the given parent.
1307      * @param newParent new parent.
1308      * @return empty copy.
1309      */
1310     private XsdTreeNode emptyCopy(final XsdTreeNode newParent)
1311     {
1312         int indexOfNode = this.parent.children.indexOf(this);
1313         if (newParent.equals(this.parent))
1314         {
1315             indexOfNode++; // its a copy so does not matter, but in case of exceptions it is clearer the copy is below 'this'
1316         }
1317         XsdTreeNode copyNode = new XsdTreeNode(newParent, this.xsdNode, this.hiddenNodes, this.referringXsdNode);
1318         if (newParent.children == null)
1319         {
1320             newParent.children = new ArrayList<>();
1321         }
1322         newParent.children.add(indexOfNode, copyNode);
1323         copyNode.parent = newParent;
1324         return copyNode;
1325     }
1326 
1327     /**
1328      * Returns whether this node can contain the information of the given node. This only checks equivalence of the underlying
1329      * XSD nodes, or equivalence of the referencing XSD nodes.
1330      * @param copied node that was copied, and may be pasted/inserted here.
1331      * @return whether this node can contain the information of the given node.
1332      */
1333     public boolean canContain(final XsdTreeNode copied)
1334     {
1335         return this.xsdNode == copied.xsdNode || (this.referringXsdNode != null && copied.referringXsdNode != null
1336                 && DocumentReader.getAttribute(this.referringXsdNode, "type").isPresent()
1337                 && DocumentReader.getAttribute(this.referringXsdNode, "type").get()
1338                         .equals(DocumentReader.getAttribute(copied.referringXsdNode, "type").orElse(null)));
1339     }
1340 
1341     /**
1342      * Copies the active status, value, choice, attributes and children of this node in to the given node.
1343      * @param copyNode node to copy data in to.
1344      */
1345     public void copyInto(final XsdTreeNode copyNode)
1346     {
1347         if (this.equals(copyNode))
1348         {
1349             return;
1350         }
1351         copyNode.value = this.value;
1352         copyNode.isIncluded = this.isIncluded;
1353         // copy choice
1354         if (this.choice != null)
1355         {
1356             XsdTreeNode choiceNode = new XsdTreeNode(copyNode.parent, this.choice.xsdNode, this.choice.hiddenNodes,
1357                     this.choice.referringXsdNode);
1358             choiceNode.choice = choiceNode;
1359             // populate options, but skip the copyNode option that was created above, insert it afterwards
1360             int selectedIndex = this.choice.options.indexOf(this);
1361             choiceNode.options = new ArrayList<>();
1362             XsdTreeNodeUtil.addChildren(this.choice.xsdNode, copyNode.parent, choiceNode.options, this.choice.hiddenNodes,
1363                     this.schema, false, selectedIndex);
1364             choiceNode.options.add(selectedIndex, copyNode);
1365             choiceNode.selected = choiceNode.options.get(selectedIndex);
1366             if (this.choice.getNodeName().equals("xsd:all"))
1367             {
1368                 XsdTreeNodeUtil.addXsdAllValidator(choiceNode, choiceNode);
1369             }
1370             for (int index = 0; index < choiceNode.options.size(); index++)
1371             {
1372                 XsdTreeNode option = choiceNode.options.get(index);
1373                 if (this.choice.getNodeName().equals("xsd:all"))
1374                 {
1375                     XsdTreeNodeUtil.addXsdAllValidator(choiceNode, option);
1376                 }
1377                 option.minOccurs = choiceNode.minOccurs;
1378                 option.maxOccurs = choiceNode.maxOccurs;
1379                 if (choiceNode.minOccurs > 0)
1380                 {
1381                     option.setActive();
1382                 }
1383                 option.active = this.choice.options.get(index).active;
1384                 option.choice = choiceNode;
1385             }
1386         }
1387         // copy attributes
1388         copyNode.assureAttributesAndDescription();
1389         for (int index = 0; index < attributeCount(); index++)
1390         {
1391             copyNode.attributeValues.set(index, this.attributeValues.get(index));
1392         }
1393         // copy children, recursive
1394         if (copyNode.children != null)
1395         {
1396             for (int index = 0; index < copyNode.getChildCount(); index++)
1397             {
1398                 XsdTreeNode child = copyNode.getChild(index);
1399                 copyNode.children.remove(index);
1400                 child.parent = null;
1401                 getRoot().fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {child, copyNode, index});
1402             }
1403         }
1404         if (this.children != null)
1405         {
1406             for (int index = 0; index < this.children.size(); index++)
1407             {
1408                 this.children.get(index).duplicate(copyNode);
1409             }
1410         }
1411     }
1412 
1413     /**
1414      * Returns whether this node may be removed without violating "minOccurs" constraints, including deactivating nodes with
1415      * minOccurs = 0.
1416      * @return whether this node may be removed without violating "minOccurs" constraints.
1417      */
1418     public boolean isRemovable()
1419     {
1420         return isActive() && siblingPositions().size() > this.minOccurs;
1421     }
1422 
1423     /**
1424      * Removes this node from the tree structure. For nodes with minOccurs = 0 that are the last of their type in the context of
1425      * their parent, the node is deactivated rather than removed. This method also explicitly removes all children nodes
1426      * recursively.
1427      */
1428     public final void remove()
1429     {
1430         int numberOfTypeOrChoiceInParent = siblingPositions().size();
1431         if (this.minOccurs == 0 && numberOfTypeOrChoiceInParent == 1 && !this.isIncluded)
1432         {
1433             setInactive();
1434             return;
1435         }
1436         if (this.choice != null && this.choice.selected.equals(this))
1437         {
1438             for (XsdTreeNode option : this.choice.options)
1439             {
1440                 if (!this.choice.selected.equals(this))
1441                 {
1442                     option.remove();
1443                 }
1444             }
1445         }
1446         removeChildren();
1447         XsdTreeNode parentNode = this.parent;
1448         int index = this.parent.children.indexOf(this);
1449         this.parent.children.remove(this);
1450         XsdTreeNodeRoot root = getRoot(); // can't get it later as we set parent to null
1451         this.parent = null;
1452         root.fireEvent(XsdTreeNodeRoot.NODE_REMOVED, new Object[] {this, parentNode, index});
1453     }
1454 
1455     /**
1456      * Removes all children.
1457      */
1458     private void removeChildren()
1459     {
1460         if (this.children != null)
1461         {
1462             // copy to prevent ConcurrentModificationException as child removes itself from this node
1463             List<XsdTreeNode> childs = new ArrayList<>(this.children);
1464             for (XsdTreeNode child : childs)
1465             {
1466                 child.remove();
1467             }
1468         }
1469     }
1470 
1471     /**
1472      * Returns whether the node can be moved up in the parent.
1473      * @return whether the node can be moved up in the parent.
1474      */
1475     public boolean canMoveUp()
1476     {
1477         List<Integer> positions = siblingPositions();
1478         return !positions.isEmpty() && this.parent.children.indexOf(this) > positions.get(0);
1479     }
1480 
1481     /**
1482      * Returns whether the node can be moved down in the parent.
1483      * @return whether the node can be moved down in the parent.
1484      */
1485     public boolean canMoveDown()
1486     {
1487         List<Integer> positions = siblingPositions();
1488         // In case of Java 21: ... < positions.getLast();
1489         return !positions.isEmpty() && this.parent.children.indexOf(this) < positions.get(positions.size() - 1);
1490     }
1491 
1492     /**
1493      * Returns an ordered list of indices within the parents child list, regarding sibling nodes of the same type. What is
1494      * considered the same type differs between a choice node, and a regular node. In case of a choice, all siblings that have a
1495      * type equal to <i>any</i> of the choice options, are considered siblings. They are all instances of the same choice,
1496      * although they are different options. If the siblings are not consecutive within the parent, nor are the returned indices.
1497      * @return list of indices within the parents child list, regarding sibling nodes of the same type.
1498      */
1499     private List<Integer> siblingPositions()
1500     {
1501         List<Integer> siblingPositions = new ArrayList<>();
1502         if (this.parent == null)
1503         {
1504             return siblingPositions;
1505         }
1506         if (this.choice != null)
1507         {
1508             for (int index = 0; index < this.parent.children.size(); index++)
1509             {
1510                 for (XsdTreeNode option : this.choice.options)
1511                 {
1512                     if (XsdTreeNodeUtil.haveSameType(option, this.parent.children.get(index)))
1513                     {
1514                         siblingPositions.add(index);
1515                         break;
1516                     }
1517                 }
1518             }
1519         }
1520         else
1521         {
1522             for (int index = 0; index < this.parent.children.size(); index++)
1523             {
1524                 if (XsdTreeNodeUtil.haveSameType(this, this.parent.children.get(index)))
1525                 {
1526                     siblingPositions.add(index);
1527                 }
1528             }
1529         }
1530         return siblingPositions;
1531     }
1532 
1533     /**
1534      * Move the node to a different position in the parent, relative to the current position. It is not checked whether this is
1535      * allowed.
1536      * @param down number of moves down. May be negative for up.
1537      */
1538     public void move(final int down)
1539     {
1540         int oldIndex = this.parent.children.indexOf(this);
1541         this.parent.children.remove(this);
1542         int newIndex = oldIndex + down;
1543         this.parent.children.add(newIndex, this);
1544         fireEvent(MOVED, new Object[] {this, oldIndex, newIndex});
1545     }
1546 
1547     /**
1548      * Returns the minimum number of this element under the parent node, as defined in minOccurs in XSD.
1549      * @return minimum number of this element under the parent node, as defined in minOccurs in XSD.
1550      */
1551     public int minOccurs()
1552     {
1553         return this.minOccurs;
1554     }
1555 
1556     /**
1557      * Returns the maximum number of this element under the parent node, as defined in maxOccurs in XSD. The XSD value
1558      * "unbounded" results in a value of -1.
1559      * @return maximum number of this element under the parent node, as defined in maxOccurs in XSD.
1560      */
1561     public int maxOccurs()
1562     {
1563         return this.maxOccurs;
1564     }
1565 
1566     /**
1567      * Returns the base type of the attribute, e.g. xsd:double.
1568      * @param index attribute index.
1569      * @return base type of the attribute, e.g. xsd:double.
1570      */
1571     public String getAttributeBaseType(final int index)
1572     {
1573         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
1574         {
1575             return "xsd:anyURI";
1576         }
1577         return ValueValidator.getBaseType(this.attributeNodes.get(index), this.schema);
1578     }
1579 
1580     /**
1581      * Returns whether this node is of the type defined by the path.
1582      * @param path path of the type in dotted xpath notation, e.g. "SignalGroup.TrafficLight".
1583      * @return whether this node is of the type defined by the path.
1584      */
1585     public boolean isType(final String path)
1586     {
1587         boolean isType = getPathString().endsWith("." + path);
1588         if (isType)
1589         {
1590             return isType;
1591         }
1592         int dot = path.lastIndexOf(".");
1593         if (dot > -1)
1594         {
1595             if (this.parent == null)
1596             {
1597                 return false; // Node was deleted, but is still visible in the GUI tree for a moment
1598             }
1599             isType = isType(path.substring(dot + 1)) && this.parent.isType(path.substring(0, dot));
1600             if (isType)
1601             {
1602                 return isType;
1603             }
1604         }
1605         return this.schema.isType(this.xsdNode, path);
1606     }
1607 
1608     // ====== Expression ======
1609 
1610     /**
1611      * Returns whether the value, any of the attributes, or any of the sub-elements, has an expression.
1612      * @return whether the node has an expression.
1613      */
1614     public boolean hasExpression()
1615     {
1616         if (!this.active)
1617         {
1618             return false;
1619         }
1620         if (valueIsExpression())
1621         {
1622             return true;
1623         }
1624         for (int index = 0; index < attributeCount(); index++)
1625         {
1626             if (attributeIsExpression(index))
1627             {
1628                 return true;
1629             }
1630         }
1631         if (this.children != null)
1632         {
1633             for (XsdTreeNode child : this.children)
1634             {
1635                 if (child.hasExpression())
1636                 {
1637                     return true;
1638                 }
1639             }
1640         }
1641         return false;
1642     }
1643 
1644     /**
1645      * Returns whether the value is an expression.
1646      * @return whether the value is an expression.
1647      */
1648     public boolean valueIsExpression()
1649     {
1650         return this.value != null && this.value.startsWith("{") && this.value.endsWith("}");
1651     }
1652 
1653     /**
1654      * Returns whether the Id is an expression.
1655      * @return whether the Id is an expression.
1656      * @throws NoSuchElementException when the node has no id attribute
1657      */
1658     public boolean idIsExpression()
1659     {
1660         Throw.when(!isIdentifiable(), NoSuchElementException.class, "Node is non-identifiable.");
1661         return attributeIsExpression(getAttributeIndexByName("Id"));
1662     }
1663 
1664     /**
1665      * Returns whether the attribute is an expression.
1666      * @param index attribute index.
1667      * @return whether the attribute is an expression.
1668      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
1669      */
1670     public boolean attributeIsExpression(final int index)
1671     {
1672         Objects.checkIndex(index, attributeCount());
1673         String attributeValue = this.attributeValues.get(index);
1674         return attributeValue != null && attributeValue.startsWith("{") && attributeValue.endsWith("}");
1675     }
1676 
1677     // ====== Consumers ======
1678 
1679     /**
1680      * A consumer can be set externally and will receive this node when its menu item is selected.
1681      * @param menuItem name of item as presented to the user.
1682      * @param consumer editor.
1683      */
1684     public void addConsumer(final String menuItem, final Consumer<XsdTreeNode> consumer)
1685     {
1686         this.consumers.put(menuItem, consumer);
1687     }
1688 
1689     /**
1690      * Returns whether this node has any consumers.
1691      * @return whether this node has any consumers.
1692      */
1693     public boolean hasConsumer()
1694     {
1695         return !this.consumers.isEmpty();
1696     }
1697 
1698     /**
1699      * Returns the menu items for which this node has consumers.
1700      * @return menu items for which this node has consumers.
1701      */
1702     public Set<String> getConsumerMenuItems()
1703     {
1704         return this.consumers.keySet();
1705     }
1706 
1707     /**
1708      * Triggers the node to be consumed.
1709      * @param menuItem menu item.
1710      * @throws IllegalArgumentException when there is no consumer for the given menu item
1711      */
1712     public void consume(final String menuItem)
1713     {
1714         Throw.when(!this.consumers.containsKey(menuItem), IllegalArgumentException.class, "Unable to consume node for %s.",
1715                 menuItem);
1716         this.consumers.get(menuItem).accept(this);
1717     }
1718 
1719     // ====== String representations ======
1720 
1721     /**
1722      * Returns the name of this node, as appropriate in XML. Examples are Node, RoadLayout, and TacticalPlanner. Most typically
1723      * this is the "name" attribute of an xsd:element. In other cases it is the ref={ref} attribute of the referring
1724      * {@code Node}. In rare cases it is "xi:include".
1725      * @return name of this node, as appropriate in XML.
1726      */
1727     public String getNodeName()
1728     {
1729         Node node = getRelevantNode();
1730         Optional<String> ref = DocumentReader.getAttribute(node, "ref");
1731         if (ref.isPresent())
1732         {
1733             return ref.get().replace("ots:", "");
1734         }
1735         Optional<String> name = DocumentReader.getAttribute(node, "name");
1736         if (name.isPresent())
1737         {
1738             return name.get().replace("ots:", "");
1739         }
1740         return node.getNodeName().replace("ots:", "");
1741     }
1742 
1743     /**
1744      * Returns the description of this node.
1745      * @return description of this node, empty if there is none.
1746      */
1747     public Optional<String> getDescription()
1748     {
1749         assureAttributesAndDescription();
1750         if (this.description == null && isChoice())
1751         {
1752             this.choice.assureAttributesAndDescription();
1753             return Optional.ofNullable(this.choice.description);
1754         }
1755         return Optional.ofNullable(this.description);
1756     }
1757 
1758     /**
1759      * Returns a string that is the name of the node, without any additional information on id and additional string function.
1760      * @return string that is the name of the node.
1761      */
1762     public String getShortString()
1763     {
1764         if (this.options != null)
1765         {
1766             // this name may appear as part of a sequence which is an option for an xsd:choice or xsd:all
1767             return this.options.toString().toLowerCase();
1768         }
1769         if (this.xsdNode.getNodeName().equals("xsd:sequence"))
1770         {
1771             // this name may appear as an option for an xsd:choice or xsd:all
1772             StringBuilder stringBuilder = new StringBuilder("{");
1773             Node relevantNode = getRelevantNode();
1774             Optional<String> annotation = NodeAnnotation.APPINFO_NAME.get(relevantNode);
1775             if (annotation.isPresent())
1776             {
1777                 stringBuilder.append(annotation);
1778             }
1779             else
1780             {
1781                 // no name for sequence specified in XSD, build one from child elements (per type to prevent repetition).
1782                 Set<String> coveredTypes = new LinkedHashSet<>();
1783                 String separator = "";
1784                 boolean preActive = this.active; // need to make sure the element has children to get the names of
1785                 this.active = true;
1786                 assureChildren();
1787                 if (getChildCount() == 1)
1788                 {
1789                     stringBuilder.append(getChild(0).getShortString());
1790                 }
1791                 else
1792                 {
1793                     for (XsdTreeNode child : this.children)
1794                     {
1795                         if (!coveredTypes.contains(child.getPathString()) || child.xsdNode.getNodeName().equals("xsd:sequence")
1796                                 || child.xsdNode.getNodeName().equals("xsd:choice")
1797                                 || child.xsdNode.getNodeName().equals("xsd:all"))
1798                         {
1799                             stringBuilder.append(separator).append(child.getShortString());
1800                             separator = "\u2009|\u2009"; // thin space | thin space
1801                             coveredTypes.add(child.getPathString());
1802                         }
1803                     }
1804                 }
1805                 this.active = preActive;
1806             }
1807             stringBuilder.append("}");
1808             if (stringBuilder.length() > MAX_OPTIONNAME_LENGTH)
1809             {
1810                 return stringBuilder.substring(0, MAX_OPTIONNAME_LENGTH - 3) + "..}";
1811             }
1812             return stringBuilder.toString();
1813         }
1814         if (this.xsdNode.getNodeName().equals("xi:include"))
1815         {
1816             return "Include";
1817         }
1818         return OtsAnimationPanel.separatedName(getNodeName());
1819     }
1820 
1821     /**
1822      * This function can be set externally and supplies an additional {@code String} to clarify this node in the tree. It is
1823      * presented after the regular node name between parenthesis. E.g. "Od Options Item (Global)" indicating the applicability
1824      * of the option.
1825      * @param stringFunction Function&lt;XsdTreeNode, String&gt; string function.
1826      * @param overwrite overwrite existing. When {@code true}, a possible existing string function is overwritten.
1827      */
1828     @SuppressWarnings("hiddenfield")
1829     public void setStringFunction(final Function<XsdTreeNode, String> stringFunction, final boolean overwrite)
1830     {
1831         if (this.stringFunction == null || overwrite)
1832         {
1833             this.stringFunction = stringFunction;
1834         }
1835     }
1836 
1837     /**
1838      * Returns the path string of this element, e.g.&nbsp;"Ots.Definitions.RoadLayouts". This is used to identify each unique
1839      * type of element.
1840      * @return path string of this element, e.g. "Ots.Definitions.RoadLayouts".
1841      */
1842     public String getPathString()
1843     {
1844         return this.pathString;
1845     }
1846 
1847     /**
1848      * Returns the short string, appended with any additional information on id and optional string function.
1849      * @return short string, appended with any additional information on id and optional string function.
1850      */
1851     @Override
1852     public String toString()
1853     {
1854         StringBuilder string = new StringBuilder(getShortString());
1855         if (!this.active)
1856         {
1857             return string.toString();
1858         }
1859         if (isIdentifiable() && getId() != null && !getId().isEmpty())
1860         {
1861             string.append("\u2009").append(getId()); // thin space
1862         }
1863         /*-
1864         if (this.minOccurs == this.maxOccurs)
1865         {
1866             string.append(" [").append(this.minOccurs).append("]");
1867         }
1868         else
1869         {
1870             string.append(" [").append(this.minOccurs).append("..")
1871                     .append(this.maxOccurs == -1 ? "\u221E" : Integer.toString(this.maxOccurs)).append("]"); // u221E = infinite
1872         }
1873         */
1874         if (this.stringFunction != null)
1875         {
1876             string.append("\u2009(").append(this.stringFunction.apply(this)).append(")"); // u2009 = thin space
1877         }
1878         return string.toString();
1879     }
1880 
1881     // ====== Restrictions ======
1882 
1883     /**
1884      * Returns all restrictions for Id attribute. These are not sorted and may contain duplicates. Id restrictions may be valid
1885      * if the Id field points to another element.
1886      * @return list of restrictions for the Id.
1887      */
1888     public List<String> getIdRestrictions()
1889     {
1890         return getAttributeRestrictions(this.idIndex);
1891     }
1892 
1893     /**
1894      * Returns all restrictions for the value. These are not sorted and may contain duplicates.
1895      * @return list of restrictions for the value.
1896      */
1897     public List<String> getValueRestrictions()
1898     {
1899         if ("ots:boolean".equals(DocumentReader.getAttribute(getRelevantNode(), "type").orElse(null)))
1900         {
1901             return List.of("true", "false");
1902         }
1903         List<String> valueOptions = getOptionsFromValidators(this.valueValidators);
1904         if (!valueOptions.isEmpty())
1905         {
1906             return valueOptions;
1907         }
1908         return XsdTreeNodeUtil.getOptionsFromRestrictions(ValueValidator.getRestrictions(this.xsdNode, this.schema));
1909     }
1910 
1911     /**
1912      * Returns all restrictions for the given attribute. These are not sorted and may contain duplicates.
1913      * @param index attribute number.
1914      * @return list of restrictions for the attribute.
1915      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
1916      */
1917     public List<String> getAttributeRestrictions(final int index)
1918     {
1919         Objects.checkIndex(index, attributeCount());
1920         if ("ots:boolean".equals(DocumentReader.getAttribute(this.attributeNodes.get(index), "type").orElse(null)))
1921         {
1922             return List.of("true", "false");
1923         }
1924         String field = getAttributeNameByIndex(index);
1925         // Gather all validators on the attribute, and then their respective field objects
1926         Map<ValueValidator, Object> map = new LinkedHashMap<>();
1927         this.attributeValidators.computeIfAbsent(field, (f) -> new TreeSet<>())
1928                 .forEach((v) -> map.put(v, this.attributeValidatorFields.get(field).get(v)));
1929         List<String> valueOptions = getOptionsFromValidators(map);
1930         // Accept empty list on include node, as getOptionsFromRestrictions() cannot deal with include nodes, who's attributes
1931         // (File and Fallback) have no options anyway.
1932         if (!valueOptions.isEmpty() || this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
1933         {
1934             return valueOptions;
1935         }
1936         return XsdTreeNodeUtil
1937                 .getOptionsFromRestrictions(ValueValidator.getRestrictions(this.attributeNodes.get(index), this.schema));
1938     }
1939 
1940     /**
1941      * Returns options based on a set of validators. This is the intersection of options each validator allows.
1942      * @param validators validators and their respective fields.
1943      * @return list of options.
1944      */
1945     private List<String> getOptionsFromValidators(final Map<ValueValidator, Object> validators)
1946     {
1947         List<String> intersection = null;
1948         for (Entry<ValueValidator, Object> entry : validators.entrySet())
1949         {
1950             ValueValidator validator = entry.getKey();
1951             Optional<List<String>> valueOptions = validator.getOptions(this, entry.getValue());
1952             if (valueOptions.isPresent())
1953             {
1954                 intersection = intersection == null ? valueOptions.get()
1955                         : intersection.stream().filter(valueOptions.get()::contains).collect(Collectors.toList());
1956             }
1957         }
1958         return intersection == null ? Collections.emptyList() : intersection;
1959     }
1960 
1961     // ====== Coupling ======
1962 
1963     /**
1964      * Returns the node to which the value refers via a {@link CoupledValidator}.
1965      * @return node to which the value refers via a {@link CoupledValidator}, empty if no such node.
1966      */
1967     public Optional<XsdTreeNode> getCoupledNodeValue()
1968     {
1969         return Optional.ofNullable(getCoupledNode(this.valueValidators.keySet()));
1970     }
1971 
1972     /**
1973      * Returns the node to which an attribute refers via a {@link CoupledValidator}.
1974      * @param index index of the attribute.
1975      * @return node to which an attribute refers via a {@link CoupledValidator}, empty if no such node.
1976      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
1977      */
1978     public Optional<XsdTreeNode> getCoupledNodeAttribute(final int index)
1979     {
1980         Objects.checkIndex(index, attributeCount());
1981         return getCoupledNodeAttribute(getAttributeNameByIndex(index));
1982     }
1983 
1984     /**
1985      * Returns the node to which an attribute refers via a {@link CoupledValidator}.
1986      * @param attribute attribute name.
1987      * @return node to which an attribute refers via a {@link CoupledValidator}, empty if no such node.
1988      */
1989     public Optional<XsdTreeNode> getCoupledNodeAttribute(final String attribute)
1990     {
1991         if (this.attributeValidators.containsKey(attribute))
1992         {
1993             return Optional.ofNullable(getCoupledNode(this.attributeValidators.get(attribute)));
1994         }
1995         return Optional.empty();
1996     }
1997 
1998     /**
1999      * Return coupled node via a coupled validator.
2000      * @param validators validators.
2001      * @return coupled node via a coupled validator, or {@code null} if no such node.
2002      */
2003     private XsdTreeNode getCoupledNode(final Set<ValueValidator> validators)
2004     {
2005         for (ValueValidator validator : validators)
2006         {
2007             if (validator instanceof CoupledValidator coupledValidator)
2008             {
2009                 coupledValidator.validate(this); // to trigger finding the right node should value have changed
2010                 return coupledValidator.getCoupledNode(this);
2011             }
2012         }
2013         return null;
2014     }
2015 
2016     // ====== Validation ======
2017 
2018     /**
2019      * Returns whether the contents of the attributes, value and other aspects of the node itself are valid. This excludes child
2020      * nodes.
2021      * @return whether the contents of the attributes, value and other aspects of the node itself are valid.
2022      */
2023     public boolean isSelfValid()
2024     {
2025         if (this.isSelfValid == null)
2026         {
2027             if (!this.active)
2028             {
2029                 this.isSelfValid = true;
2030             }
2031             else if (reportInvalidNode().isPresent() || reportInvalidValue().isPresent())
2032             {
2033                 this.isSelfValid = false;
2034             }
2035             else
2036             {
2037                 boolean attributesValid = true;
2038                 for (int index = 0; index < attributeCount(); index++)
2039                 {
2040                     if (reportInvalidAttributeValue(index).isPresent())
2041                     {
2042                         attributesValid = false;
2043                         break;
2044                     }
2045                 }
2046                 this.isSelfValid = attributesValid;
2047             }
2048         }
2049         return this.isSelfValid;
2050     }
2051 
2052     /**
2053      * Returns whether the node, and all its children recursively, is valid. This means all required values are supplied, and
2054      * all supplied values comply to their respective types and constraints.
2055      * @return whether the node is valid.
2056      */
2057     public boolean isValid()
2058     {
2059         if (this.isValid == null)
2060         {
2061             if (!isActive())
2062             {
2063                 this.isValid = true;
2064             }
2065             else if (!isSelfValid())
2066             {
2067                 this.isValid = false;
2068             }
2069             else
2070             {
2071                 boolean childrenValid = true;
2072                 if (this.children != null)
2073                 {
2074                     for (XsdTreeNode child : this.children)
2075                     {
2076                         if (!child.isValid())
2077                         {
2078                             childrenValid = false;
2079                         }
2080                     }
2081                 }
2082                 this.isValid = childrenValid;
2083             }
2084         }
2085         return this.isValid;
2086     }
2087 
2088     /**
2089      * Sets the valid status of this node and all parent nodes to unknown.
2090      */
2091     public void invalidate()
2092     {
2093         this.isSelfValid = null;
2094         this.isValid = null;
2095         if (this.parent != null)
2096         {
2097             this.parent.invalidate();
2098         }
2099         this.valueValid = null;
2100         this.valueInvalidMessage = null;
2101         this.nodeValid = null;
2102         this.nodeInvalidMessage = null;
2103         assureAttributesAndDescription();
2104         Collections.fill(this.attributeValid, null);
2105         Collections.fill(this.attributeInvalidMessage, null);
2106     }
2107 
2108     /**
2109      * Invalidates entire tree in a nested manner. Triggered after the path of the current file changes in the root node.
2110      */
2111     void invalidateAll()
2112     {
2113         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
2114         {
2115             removeChildren();
2116             this.children = null;
2117             assureChildren();
2118         }
2119         else if (this.children != null)
2120         {
2121             for (XsdTreeNode child : this.children)
2122             {
2123                 child.invalidateAll();
2124             }
2125         }
2126         invalidate();
2127     }
2128 
2129     /**
2130      * Adds a validator for the node.
2131      * @param validator validator.
2132      * @throws IllegalStateException if a CoupledValidator is added while one was already added to the value before
2133      */
2134     public void addNodeValidator(final Function<XsdTreeNode, String> validator)
2135     {
2136         this.nodeValidators.add(validator);
2137     }
2138 
2139     /**
2140      * Adds a validator for the value. The field object is any object that is returned to the validator in its
2141      * {@code getOptions()} method, such that it can know for which field option values should be given.
2142      * @param validator validator.
2143      * @param field field.
2144      * @throws IllegalStateException if a CoupledValidator is added while one was already added to the value before
2145      */
2146     public void addValueValidator(final ValueValidator validator, final Object field)
2147     {
2148         Throw.when(validator instanceof CoupledValidator && coupledValidatorExists(this.valueValidators.keySet(), validator),
2149                 IllegalStateException.class, "Adding %s to the node value of %s but a CoupledValidator already exists.",
2150                 validator.getClass().getSimpleName(), getPathString());
2151         this.valueValidators.put(validator, field);
2152     }
2153 
2154     /**
2155      * Adds a validator for the value of an attribute.
2156      * @param attribute attribute name.
2157      * @param validator validator.
2158      * @throws IllegalStateException if a CoupledValidator is added while one was already added to the attribute before
2159      */
2160     public void addAttributeValidator(final String attribute, final ValueValidator validator)
2161     {
2162         addAttributeValidator(attribute, validator, null);
2163     }
2164 
2165     /**
2166      * Adds a validator for the value of an attribute. The field object is any object that is returned to the validator in its
2167      * {@code getOptions()} method, such that it can know for which field option values should be given.
2168      * @param attribute attribute name.
2169      * @param validator validator.
2170      * @param field field.
2171      * @throws IllegalStateException if a CoupledValidator is added while one was already added to the attribute before
2172      */
2173     public void addAttributeValidator(final String attribute, final ValueValidator validator, final Object field)
2174     {
2175         Throw.when(
2176                 validator instanceof CoupledValidator
2177                         && coupledValidatorExists(this.attributeValidators.get(attribute), validator),
2178                 IllegalStateException.class, "Adding %s to the attribute %s in %s but a CoupledValidator already exists.",
2179                 validator.getClass().getSimpleName(), attribute, getPathString());
2180         this.attributeValidators.computeIfAbsent(attribute, (key) -> new TreeSet<>()).add(validator);
2181         this.attributeValidatorFields.computeIfAbsent(attribute, (key) -> new LinkedHashMap<>()).put(validator, field);
2182     }
2183 
2184     /**
2185      * Returns {@code true} if the set of validators contains a {@code CoupledValidator}.
2186      * @param validators validators
2187      * @param validator validator that is about to be added
2188      * @return {@code true} if the set of validators contains a {@code CoupledValidator}
2189      */
2190     private boolean coupledValidatorExists(final Set<ValueValidator> validators, final ValueValidator validator)
2191     {
2192         return validators != null
2193                 && validators.stream().filter((v) -> v instanceof CoupledValidator && !v.equals(validator)).count() > 0;
2194     }
2195 
2196     /**
2197      * Returns a message why the node is invalid, empty if it is valid. This only concerns validators on node level, i.e. not on
2198      * attribute or value level. E.g. because the node is duplicate in its parent.
2199      * @return message why the id is invalid, empty if it is valid.
2200      */
2201     public Optional<String> reportInvalidNode()
2202     {
2203         if (this.nodeValid == null)
2204         {
2205             for (Function<XsdTreeNode, String> validator : this.nodeValidators)
2206             {
2207                 String message = validator.apply(this);
2208                 if (message != null)
2209                 {
2210                     this.nodeInvalidMessage = message;
2211                     this.nodeValid = false;
2212                     return Optional.of(message);
2213                 }
2214             }
2215             this.nodeValid = true;
2216         }
2217         return Optional.ofNullable(this.nodeInvalidMessage);
2218     }
2219 
2220     /**
2221      * Returns a message why the id is invalid, empty if it is valid.
2222      * @return message why the id is invalid, empty if it is valid.
2223      */
2224     public Optional<String> reportInvalidId()
2225     {
2226         if (!isActive())
2227         {
2228             return Optional.empty();
2229         }
2230         return isIdentifiable() ? reportInvalidAttributeValue(getAttributeIndexByName("Id")) : Optional.empty();
2231     }
2232 
2233     /**
2234      * Returns a message why the value is invalid, empty if it is valid.
2235      * @return message why the value is invalid, empty if it is valid.
2236      */
2237     public Optional<String> reportInvalidValue()
2238     {
2239         if (this.valueValid == null)
2240         {
2241             if (!isEditable() || !isActive())
2242             {
2243                 this.valueInvalidMessage = null;
2244                 this.valueValid = true;
2245                 return Optional.empty();
2246             }
2247             if (this.value != null && !this.value.isEmpty())
2248             {
2249                 for (ValueValidator validator : this.valueValidators.keySet())
2250                 {
2251                     Optional<String> message = validator.validate(this);
2252                     if (message.isPresent())
2253                     {
2254                         this.valueInvalidMessage = message.get();
2255                         this.valueValid = false;
2256                         return message;
2257                     }
2258                 }
2259             }
2260             Optional<String> message = ValueValidator.reportInvalidValue(this.xsdNode, this.value, this.schema);
2261             this.valueInvalidMessage = message.orElse(null);
2262             this.valueValid = message.isEmpty();
2263         }
2264         return Optional.ofNullable(this.valueInvalidMessage);
2265     }
2266 
2267     /**
2268      * Returns a message why the attribute value is invalid, empty if it is valid.
2269      * @param index index of the attribute.
2270      * @return message why the attribute value is invalid, empty if it is valid.
2271      * @throws IndexOutOfBoundsException when the index is not in the bounds for the number of attributes
2272      */
2273     public Optional<String> reportInvalidAttributeValue(final int index)
2274     {
2275         Objects.checkIndex(index, attributeCount());
2276         if (this.attributeValid.get(index) == null)
2277         {
2278             if (!isActive())
2279             {
2280                 this.attributeInvalidMessage.set(index, null);
2281                 this.attributeValid.set(index, true);
2282                 return Optional.empty();
2283             }
2284             if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
2285             {
2286                 XsdTreeNode root = getPath().get(0);
2287                 if (root instanceof XsdTreeNodeRoot)
2288                 {
2289                     Optional<String> message = ValueValidator.reportInvalidInclude(((XsdTreeNodeRoot) root).getDirectory(),
2290                             this.attributeValues.get(0), this.attributeValues.get(1));
2291                     this.attributeInvalidMessage.set(index, message.orElse(null));
2292                     this.attributeValid.set(index, message.isEmpty());
2293                     return message;
2294                 }
2295                 else
2296                 {
2297                     // node is being deleted and has no parent anymore
2298                     this.attributeInvalidMessage.set(index, null);
2299                     this.attributeValid.set(index, true);
2300                     return Optional.empty();
2301                 }
2302             }
2303             String attribute = getAttributeNameByIndex(index);
2304             for (ValueValidator validator : this.attributeValidators.computeIfAbsent(attribute, (key) -> new TreeSet<>()))
2305             {
2306                 Optional<String> message = validator.validate(this);
2307                 if (message.isPresent())
2308                 {
2309                     this.attributeInvalidMessage.set(index, message.orElse(null));
2310                     this.attributeValid.set(index, false);
2311                     return message;
2312                 }
2313             }
2314             Optional<String> message =
2315                     ValueValidator.reportInvalidAttributeValue(getAttributeNode(index), getAttributeValue(index), this.schema);
2316             this.attributeInvalidMessage.set(index, message.orElse(null));
2317             this.attributeValid.set(index, message.isEmpty());
2318             return message;
2319         }
2320         return Optional.ofNullable(this.attributeInvalidMessage.get(index));
2321     }
2322 
2323     // ====== Save / load ======
2324 
2325     /**
2326      * Saves the content of this node in a new XML element under the given XML parent. This involves a value, attributes, and
2327      * children. Children are further saved in a recursive manner. If this node is not active, this method does nothing.<br>
2328      * <br>
2329      * If this node represents a sequence as a choice option, all children are saved directly under the given XML parent node,
2330      * and no information of this node itself is saved (as such nodes have no attributes or value). In other words, in that case
2331      * this node is only a virtual layer presented to the user, but does not result in a layer in XML.
2332      * @param document used to create the new XML node.
2333      * @param xmlParent parent XML node.
2334      */
2335     public void saveXmlNodes(final Document document, final Node xmlParent)
2336     {
2337         if (!this.active)
2338         {
2339             return;
2340         }
2341 
2342         // xi:include
2343         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
2344         {
2345             Element element = document.createElement(getNodeName());
2346             xmlParent.appendChild(element);
2347             if (this.attributeValues != null && this.attributeValues.get(0) != null)
2348             {
2349                 element.setAttribute("href", this.attributeValues.get(0));
2350                 if (this.attributeValues.get(1) != null)
2351                 {
2352                     Element fallback = document.createElement("xi:fallback");
2353                     element.appendChild(fallback);
2354                     Element include = document.createElement("xi:include");
2355                     fallback.appendChild(include);
2356                     include.setAttribute("href", this.attributeValues.get(1));
2357                 }
2358             }
2359             return;
2360         }
2361 
2362         // sequences do not add a level in the xml, forward directly under parent
2363         if (this.xsdNode.getNodeName().equals("xsd:sequence"))
2364         {
2365             for (int index = 0; index < this.getChildCount(); index++)
2366             {
2367                 this.children.get(index).saveXmlNodes(document, xmlParent);
2368             }
2369             return;
2370         }
2371 
2372         Element element = document.createElement("ots:" + getNodeName());
2373         xmlParent.appendChild(element);
2374 
2375         if (this.value != null && !this.value.isEmpty())
2376         {
2377             element.setTextContent(this.value);
2378         }
2379 
2380         for (int index = 0; index < attributeCount(); index++)
2381         {
2382             String attributeValue = this.attributeValues.get(index);
2383             if (attributeValue != null && !attributeValue.isEmpty())
2384             {
2385                 element.setAttribute(getAttributeNameByIndex(index), attributeValue);
2386             }
2387         }
2388 
2389         for (int index = 0; index < this.getChildCount(); index++)
2390         {
2391             if (!this.children.get(index).isIncluded)
2392             {
2393                 this.children.get(index).saveXmlNodes(document, element);
2394             }
2395         }
2396     }
2397 
2398     /**
2399      * Parses the information from an XML node into this node. This entails a tag value, attributes, and children, for as far as
2400      * each of these is present. In a recursive manner, all child nodes are further loaded.
2401      * @param nodeXml node from XML.
2402      */
2403     public void loadXmlNodes(final Node nodeXml)
2404     {
2405         setActive();
2406 
2407         if (this.xsdNode.equals(XiIncludeNode.XI_INCLUDE))
2408         {
2409             assureAttributesAndDescription();
2410             setAttributeValue(0, DocumentReader.getAttribute(nodeXml, "href").orElse(null));
2411             Optional<Node> fallback = DocumentReader.getChild(nodeXml, "xi:fallback");
2412             if (fallback.isPresent())
2413             {
2414                 Optional<Node> fallbackInclude = DocumentReader.getChild(fallback.get(), "xi:include");
2415                 if (fallbackInclude.isPresent())
2416                 {
2417                     setAttributeValue(1, DocumentReader.getAttribute(fallbackInclude.get(), "href").orElse(null));
2418                 }
2419             }
2420             return;
2421         }
2422 
2423         // value
2424         String candidateValue = "";
2425         if (nodeXml.getChildNodes() != null)
2426         {
2427             for (int indexXml = 0; indexXml < nodeXml.getChildNodes().getLength(); indexXml++)
2428             {
2429                 if (nodeXml.getChildNodes().item(indexXml).getNodeName().equals("#text"))
2430                 {
2431                     String nodeValue = nodeXml.getChildNodes().item(indexXml).getNodeValue();
2432                     if (!nodeValue.isBlank())
2433                     {
2434                         candidateValue += nodeValue;
2435                     }
2436                 }
2437             }
2438         }
2439         if (!candidateValue.isEmpty())
2440         {
2441             String previous = this.value;
2442             this.value = candidateValue;
2443             fireEvent(new Event(VALUE_CHANGED, new Object[] {this, previous}));
2444         }
2445 
2446         // attributes
2447         assureAttributesAndDescription();
2448         if (nodeXml.getAttributes() != null)
2449         {
2450             for (int index = 0; index < nodeXml.getAttributes().getLength(); index++)
2451             {
2452                 Node attributeNode = nodeXml.getAttributes().item(index);
2453                 switch (attributeNode.getNodeName())
2454                 {
2455                     case "xmlns:ots":
2456                     case "xmlns:xi":
2457                     case "xmlns:xsi":
2458                     case "xsi:schemaLocation":
2459                         continue;
2460                     default:
2461                         try
2462                         {
2463                             setAttributeValue(attributeNode.getNodeName(), attributeNode.getNodeValue());
2464                         }
2465                         catch (NoSuchElementException e)
2466                         {
2467                             Logger.ots().warn("Unable to load attribute {}=\"{}\" in {}.", attributeNode.getNodeName(),
2468                                     attributeNode.getNodeValue(), getShortString());
2469                         }
2470                 }
2471             }
2472         }
2473 
2474         // children
2475         assureChildren();
2476         if (nodeXml.getChildNodes() != null)
2477         {
2478             loadChildren(new LoadingIndices(0, 0), nodeXml.getChildNodes(), false);
2479             // In included nodes, remove nodes that will not contain any of the loaded xml data. For example remove nodes that
2480             // are otherwise shown as inactive and allow a user to enable it, which makes no sense for imported nodes.
2481             if (this.isIncluded)
2482             {
2483                 int index = 0;
2484                 while (index < this.children.size())
2485                 {
2486                     boolean relevantForAny = false;
2487                     for (int indexXml = 0; indexXml < nodeXml.getChildNodes().getLength(); indexXml++)
2488                     {
2489                         String xmlName = nodeXml.getChildNodes().item(indexXml).getNodeName().replace("ots:", "");
2490                         if (this.children.get(index).isRelevantNode(xmlName))
2491                         {
2492                             relevantForAny = true;
2493                             break;
2494                         }
2495                     }
2496                     if (!relevantForAny)
2497                     {
2498                         // can't do a remove() on the node, as it might just become deactivated and still be visible
2499                         this.children.remove(index);
2500                     }
2501                     else
2502                     {
2503                         index++;
2504                     }
2505                 }
2506             }
2507 
2508         }
2509         invalidate();
2510     }
2511 
2512     /**
2513      * Parses child nodes from XML in to this node's children, as far as it can given available inactive child nodes. Note that
2514      * these child nodes are derived from the XSD schema. This method will first find a relevant node to load each child XML
2515      * node into. The relevant node can be found in two ways:
2516      * <ol>
2517      * <li>The previous child node is relevant for the XML child node. This happens when XML specifies multiple nodes of the
2518      * same type, in a sequence or choice with multiple occurrence. The previous child node will be added, such that information
2519      * of the XML child node can be loaded in to the added child node.</li>
2520      * <li>We move to the next child node until we find a node that is relevant for the XML child node. This should only skip
2521      * inactive nodes for which XML specifies no information.</li>
2522      * </ol>
2523      * Next, the information from XML is loaded in to the relevant child node. This can happen in four ways:
2524      * <ol>
2525      * <li>The relevant node is not a choice or sequence, information is loaded in to it with {@code loadXmlNodes}.</li>
2526      * <li>The relevant node is a sequence. The relevant child in the sequence is found, and all XML child nodes that can be
2527      * loaded in to it, are by calling {@code loadChildren}.</li>
2528      * <li>The relevant node is a choice, where the relevant option is not a sequence. The option will be set in the choice.
2529      * Information is loaded in to the selected option with {@code loadXmlNodes}.</li>
2530      * <li>The relevant node is a choice, where the relevant option is a sequence. The option (sequence node) will be set in the
2531      * choice. The relevant child in the sequence is found, and all XML child nodes that can be loaded in to it, are by calling
2532      * {@code loadChildren}.</li>
2533      * </ol>
2534      * Note that for case 3, the child content of a deeper {@code XsdChildNode} is defined at the same level in XML. Hence, only
2535      * some of the XML children may be loaded in the deeper level. To keep track of which XML child nodes are loaded where, the
2536      * value {@code LoadingIndices.xmlNode} is given as input (previous nodes have already been loaded at a higher level or in
2537      * another choice sequence). In this value also the index of the first XML child node that could not be loaded in the choice
2538      * sequence is returned.<br>
2539      * <br>
2540      * The parameter {@code LoadingIndices} is also used when an XML node cannot be loaded at all because it does not comply
2541      * with the XSD schema. This will cause the loading to run through all children to see whether it can be loaded there. The
2542      * second value {@code LoadingIndices.xsdTreeNode} is used as input to know where to continue in a second call to this
2543      * method after an earlier call came across an XML node that could not be loaded. In {@code LoadingIndices.xsdTreeNode} the
2544      * index of the last child node in to which XML data was loaded is given.
2545      * @param indices index of the first XML child node to load, and first XsdTreeNode index to use.
2546      * @param childrenXml list of XML child nodes as specified within one parent XML tag.
2547      * @param loadingSubSequence whether this call is loading children as a sub-sequence.
2548      */
2549     protected void loadChildren(final LoadingIndices indices, final NodeList childrenXml, final boolean loadingSubSequence)
2550     {
2551         List<XsdTreeNode> loadedChildren = new ArrayList<>(); // to skip nodes in later passes
2552         int passes = 0;
2553         int maxPasses = 1;
2554         int loadedDuringPass = 0;
2555         Optional<Node> complexType = DocumentReader.getChild(this.xsdNode, "xsd:complexType");
2556         if (complexType.isPresent())
2557         {
2558             Optional<Node> sequence = DocumentReader.getChild(complexType.get(), "xsd:sequence");
2559             if (sequence.isPresent())
2560             {
2561                 maxPasses = Occurs.MAX.get(sequence.get());
2562             }
2563         }
2564 
2565         int xmlNodeIndex = indices.getXmlNode();
2566         int xsdTreeNodeIndex = indices.getXsdTreeNode();
2567         while (xmlNodeIndex < childrenXml.getLength())
2568         {
2569             Node childNodeXml = childrenXml.item(xmlNodeIndex);
2570             if (childNodeXml.getNodeName().equals("#text"))
2571             {
2572                 xmlNodeIndex++;
2573                 continue;
2574             }
2575 
2576             // find relevant node: previous node, or skip to next until we find the relevant node
2577             String nameXml = childNodeXml.getNodeName().replace("ots:", "");
2578             if (xsdTreeNodeIndex > 0 && this.children.get(xsdTreeNodeIndex - 1).isRelevantNode(nameXml))
2579             {
2580                 if (xsdTreeNodeIndex >= this.children.size() || !this.children.get(xsdTreeNodeIndex).isRelevantNode(nameXml))
2581                 {
2582                     this.children.get(xsdTreeNodeIndex - 1).add();
2583                 }
2584             }
2585             else
2586             {
2587                 while (xsdTreeNodeIndex < this.children.size() && (!this.children.get(xsdTreeNodeIndex).isRelevantNode(nameXml)
2588                         || loadedChildren.contains(this.children.get(xsdTreeNodeIndex))))
2589                 {
2590                     xsdTreeNodeIndex++;
2591                 }
2592                 if (xsdTreeNodeIndex >= this.children.size())
2593                 {
2594                     if (loadedDuringPass == 0)
2595                     {
2596                         Logger.ots().warn("Failing to load {}, it is not a valid node.", nameXml);
2597                         xmlNodeIndex++;
2598                         xsdTreeNodeIndex = 0; // start next pass, without increasing # passes as to just skip the node
2599                         continue;
2600                     }
2601                     else
2602                     {
2603                         passes++;
2604                         if (passes >= maxPasses)
2605                         {
2606                             if (!loadingSubSequence) // when loading sub sequence, just escalate back the recursion
2607                             {
2608                                 Logger.ots().warn("Failing to load {}, maximum number of passes reached.", nameXml);
2609                             }
2610                             indices.setXmlNode(xmlNodeIndex);
2611                             return;
2612                         }
2613                     }
2614                     xsdTreeNodeIndex = 0; // start next pass
2615                     loadedDuringPass = 0;
2616                     continue;
2617                 }
2618             }
2619 
2620             // load information in relevant node, can be a choice, can be a sequence in a choice
2621             XsdTreeNode relevantChild = this.children.get(xsdTreeNodeIndex);
2622             if (relevantChild.choice == null)
2623             {
2624                 if (relevantChild.getNodeName().equals("xsd:sequence"))
2625                 {
2626                     LoadingIndices sequenceIndices = new LoadingIndices(xmlNodeIndex, 0);
2627                     relevantChild.loadChildren(sequenceIndices, childrenXml, true); // recursion for sequence
2628                     loadedChildren.add(relevantChild);
2629                     xmlNodeIndex = sequenceIndices.getXmlNode() - 1; // continue from first not loaded (if any)
2630                 }
2631                 else
2632                 {
2633                     relevantChild.loadXmlNodes(childNodeXml); // recursion for regular node
2634                     loadedChildren.add(relevantChild);
2635                 }
2636             }
2637             else
2638             {
2639                 boolean optionSet = false;
2640                 for (XsdTreeNode option : relevantChild.choice.options)
2641                 {
2642                     if (option.xsdNode.getNodeName().equals("xsd:sequence"))
2643                     {
2644                         for (XsdTreeNode child : option.children)
2645                         {
2646                             if (child.isRelevantNode(nameXml))
2647                             {
2648                                 relevantChild.choice.setOption(option);
2649                                 LoadingIndices optionIndices = new LoadingIndices(xmlNodeIndex, 0);
2650                                 option.loadChildren(optionIndices, childrenXml, true); // recursion for option sequence
2651                                 loadedChildren.add(option);
2652                                 xmlNodeIndex = optionIndices.getXmlNode() - 1; // continue from first not loaded (if any)
2653                                 optionSet = true;
2654                                 break;
2655                             }
2656                         }
2657                     }
2658                     if (option.getNodeName().equals(nameXml))
2659                     {
2660                         relevantChild.choice.setOption(option);
2661                         option.loadXmlNodes(childNodeXml); // recursion for regular option node
2662                         optionSet = true;
2663                     }
2664                     if (optionSet)
2665                     {
2666                         for (XsdTreeNode otherOption : relevantChild.choice.options)
2667                         {
2668                             if (!otherOption.equals(option))
2669                             {
2670                                 otherOption.setActive();
2671                             }
2672                         }
2673                         break;
2674                     }
2675                 }
2676             }
2677             loadedDuringPass++;
2678             xsdTreeNodeIndex++;
2679             indices.setXsdTreeNode(xsdTreeNodeIndex);
2680             xmlNodeIndex++;
2681         }
2682         indices.setXmlNode(xmlNodeIndex);
2683     }
2684 
2685     /**
2686      * Checks whether this node is relevant to contain the information of the given tag name from XML. Being relevant means any
2687      * of the following:
2688      * <ol>
2689      * <li>The name of this node is equal to the tag, and thus directly contains the tag information.</li>
2690      * <li>This node is a sequence that has a child element that is considered relevant (in a recursive manner).</li>
2691      * <li>This node is a choice, and an option of this choice is considered relevant (in a recursive manner).</li>
2692      * <li>This node is a choice, and one of its options is a sequence that has a child element that is considered relevant (in
2693      * a recursive manner).</li>
2694      * </ol>
2695      * Given the recursive nature of 2, 3 and 4, in the end some node has a name equal to the tag from XML.
2696      * @param nameXml tag name from XML.
2697      * @return whether this node is relevant to contain the information of the given tag name from XML.
2698      */
2699     private boolean isRelevantNode(final String nameXml)
2700     {
2701         if (this.getNodeName().equals(nameXml))
2702         {
2703             return true;
2704         }
2705         if (this.choice == null)
2706         {
2707             if (getNodeName().equals("xsd:sequence"))
2708             {
2709                 this.active = true;
2710                 assureChildren();
2711                 for (XsdTreeNode child : this.children)
2712                 {
2713                     boolean relevant = child.isRelevantNode(nameXml);
2714                     if (relevant)
2715                     {
2716                         return relevant;
2717                     }
2718                 }
2719             }
2720             return false;
2721         }
2722         if (this.choice.selected.equals(this))
2723         {
2724             for (XsdTreeNode option : this.choice.options)
2725             {
2726                 if (option.xsdNode.getNodeName().equals("xsd:sequence"))
2727                 {
2728                     option.active = true;
2729                     option.assureChildren();
2730                     for (XsdTreeNode child : option.children)
2731                     {
2732                         boolean relevant = child.isRelevantNode(nameXml);
2733                         if (relevant)
2734                         {
2735                             return relevant;
2736                         }
2737                     }
2738                 }
2739                 else if (!option.equals(this))
2740                 {
2741                     boolean relevant = option.isRelevantNode(nameXml);
2742                     if (relevant)
2743                     {
2744                         return relevant;
2745                     }
2746                 }
2747             }
2748         }
2749         return false;
2750     }
2751 
2752     // ====== Listeners ======
2753 
2754     @Override
2755     public boolean addListener(final EventListener listener, final EventType eventType, final ReferenceType referenceType)
2756     {
2757         boolean result = super.addListener(listener, eventType, referenceType);
2758         sortListeners(eventType);
2759         return result;
2760     }
2761 
2762     @Override
2763     public boolean addListener(final EventListener listener, final EventType eventType)
2764     {
2765         boolean result = super.addListener(listener, eventType);
2766         sortListeners(eventType);
2767         return result;
2768     }
2769 
2770     /**
2771      * Prioritizes listeners by: Undo, KeyValidator, KeyrefValidator, CoupledValidator, any other listener. This is to support
2772      * the flow of dependencies when nodes need to be validated to create couplings. For example the RoadLayoutElementValidator
2773      * depending on the coupling between Link.DefinedRoadLayout (node value) to a Definitions.RoadLayouts.RoadLayout (Id
2774      * attribute value). However, this does not create any guarantee as the flow of actions and cascading changes is more
2775      * complex than this linear notion. For example because an undo action creates several nodes in a single action.
2776      * @param eventType event type for which to sort the listeners.
2777      */
2778     private void sortListeners(final EventType eventType)
2779     {
2780         EventListenerMap map = getEventListenerMap();
2781         Reference<EventListener> undo = null;
2782         List<Reference<EventListener>> list = map.get(eventType);
2783         List<Reference<EventListener>> keys = new ArrayList<>();
2784         List<Reference<EventListener>> keyrefs = new ArrayList<>();
2785         List<Reference<EventListener>> coupled = new ArrayList<>();
2786         for (Reference<EventListener> listen : list)
2787         {
2788             if (listen.get() instanceof Undo)
2789             {
2790                 undo = listen;
2791             }
2792             else if (listen.get() instanceof KeyValidator)
2793             {
2794                 keys.add(listen);
2795             }
2796             else if (listen.get() instanceof KeyrefValidator)
2797             {
2798                 keyrefs.add(listen);
2799             }
2800             else if (listen.get() instanceof CoupledValidator)
2801             {
2802                 coupled.add(listen);
2803             }
2804         }
2805         list.removeAll(coupled);
2806         list.removeAll(keyrefs);
2807         list.removeAll(keys);
2808         list.addAll(0, coupled);
2809         list.addAll(0, keyrefs);
2810         list.addAll(0, keys);
2811         if (undo != null)
2812         {
2813             list.remove(undo);
2814             list.add(0, undo);
2815         }
2816     }
2817 
2818 }