View Javadoc
1   package org.opentrafficsim.editor;
2   
3   import java.rmi.RemoteException;
4   import java.util.ArrayList;
5   import java.util.LinkedHashMap;
6   import java.util.List;
7   import java.util.Map;
8   import java.util.Optional;
9   
10  import org.djutils.event.Event;
11  import org.djutils.event.EventListener;
12  import org.djutils.immutablecollections.Immutable;
13  import org.djutils.immutablecollections.ImmutableArrayList;
14  import org.djutils.immutablecollections.ImmutableList;
15  import org.opentrafficsim.base.OtsRuntimeException;
16  import org.opentrafficsim.base.logger.Logger;
17  import org.opentrafficsim.editor.decoration.validation.XsdAllValidator;
18  import org.w3c.dom.Node;
19  
20  /**
21   * This class exists to keep {@code XsdTreeNode} at manageable size. It houses all static methods used in {@code XsdTreeNode}.
22   * <p>
23   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
24   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
25   * </p>
26   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
27   */
28  public final class XsdTreeNodeUtil
29  {
30  
31      /** Validators for xsd:all option nodes. This is maintained per root object (i.e. per tree) and xsd:all node. */
32      private static final Map<XsdTreeNodeRoot, Map<String, XsdAllValidator>> XSD_ALL_VALIDATORS = new LinkedHashMap<>();
33  
34      /**
35       * Private constructor.
36       */
37      private XsdTreeNodeUtil()
38      {
39  
40      }
41  
42      /**
43       * Add xsd:all validator to the given node.
44       * @param shared shared xsd:all node.
45       * @param node xsd:all node, or one of its children.
46       */
47      static void addXsdAllValidator(final XsdTreeNode shared, final XsdTreeNode node)
48      {
49          String path = shared.getPathString();
50          XsdAllValidator validator = XSD_ALL_VALIDATORS.computeIfAbsent(shared.getRoot(), (r) -> new LinkedHashMap<>())
51                  .computeIfAbsent(path, (p) -> new XsdAllValidator(node.getRoot()));
52          node.addNodeValidator(validator);
53          validator.addNode(node);
54      }
55  
56      /**
57       * Main expansion algorithm. Loops all child XSD nodes, and selects those that define next elements.
58       * @param node node to get the children of.
59       * @param parentNode parent node for the created children.
60       * @param children list to add the children to. This may be different from {@code parentNode.children} due to layered choice
61       *            structures.
62       * @param hiddenNodes nodes between the XSD node of the parent, and this tree node's XSD node.
63       * @param schema schema to get types and referred elements from.
64       * @param flattenSequence when true, treats an xsd:sequence child as an extension of the node. In the context of a choice
65       *            this should be {@code null}.
66       * @param skip child index to skip, this is used when copying choice options from an option that is already created (i.e.
67       *            {@code copyNode} in {@code XsdTreeNode.copyInto(copyNode)}).
68       */
69      static void addChildren(final Node node, final XsdTreeNode parentNode, final List<XsdTreeNode> children,
70              final ImmutableList<Node> hiddenNodes, final Schema schema, final boolean flattenSequence, final int skip)
71      {
72          int skipIndex = skip;
73          XsdTreeNode root = parentNode.getRoot();
74          for (int childIndex = 0; childIndex < node.getChildNodes().getLength(); childIndex++)
75          {
76              Node child = node.getChildNodes().item(childIndex);
77              switch (child.getNodeName())
78              {
79                  case "xsd:element":
80                      if (children.size() == skipIndex)
81                      {
82                          skipIndex = -1;
83                          break;
84                      }
85                      XsdTreeNode element;
86                      Optional<String> ref = DocumentReader.getAttribute(child, "ref");
87                      Optional<String> type = DocumentReader.getAttribute(child, "type");
88                      if (ref.isPresent())
89                      {
90                          element = new XsdTreeNode(parentNode, ref(child, ref.get(), schema), append(hiddenNodes, node), child);
91                      }
92                      else if (type.isPresent())
93                      {
94                          Node typedNode = type(child, type.get(), schema);
95                          if (typedNode == null)
96                          {
97                              // xsd:string or other basic type
98                              element = new XsdTreeNode(parentNode, child, append(hiddenNodes, node));
99                          }
100                         else
101                         {
102                             element = new XsdTreeNode(parentNode, typedNode, append(hiddenNodes, node), child);
103                         }
104                     }
105                     else
106                     {
107                         element = new XsdTreeNode(parentNode, child, append(hiddenNodes, node));
108                     }
109                     children.add(element);
110                     root.fireEvent(XsdTreeNodeRoot.NODE_CREATED,
111                             new Object[] {element, parentNode, parentNode.children.indexOf(element)});
112                     break;
113                 case "xsd:sequence":
114                     if (children.size() == skipIndex)
115                     {
116                         skipIndex = -1;
117                         break;
118                     }
119                     if (flattenSequence)
120                     {
121                         addChildren(child, parentNode, children, XsdTreeNodeUtil.append(hiddenNodes, node), schema, false, -1);
122                     }
123                     else
124                     {
125                         XsdTreeNode sequence = new XsdTreeNode(parentNode, child, append(hiddenNodes, node));
126                         children.add(sequence);
127                         root.fireEvent(XsdTreeNodeRoot.NODE_CREATED,
128                                 new Object[] {sequence, parentNode, parentNode.children.indexOf(sequence)});
129                     }
130                     break;
131                 case "xsd:choice":
132                 case "xsd:all":
133                     if (children.size() == skipIndex)
134                     {
135                         skipIndex = -1;
136                         break;
137                     }
138                     XsdTreeNode choice = new XsdTreeNode(parentNode, child, append(hiddenNodes, node));
139                     root.fireEvent(XsdTreeNodeRoot.NODE_CREATED,
140                             new Object[] {choice, parentNode, parentNode.children.indexOf(choice)});
141                     choice.createOptions();
142                     children.add(choice);
143                     choice.setOption(choice.options.get(0));
144                     break;
145                 case "xsd:extension":
146                     if (children.size() == skipIndex)
147                     {
148                         skipIndex = -1;
149                         break;
150                     }
151                     XsdTreeNode extension = new XsdTreeNode(parentNode, child, append(hiddenNodes, node));
152                     root.fireEvent(XsdTreeNodeRoot.NODE_CREATED,
153                             new Object[] {extension, parentNode, parentNode.children.indexOf(extension)});
154                     children.add(extension);
155                     break;
156                 case "xsd:attribute":
157                 case "xsd:annotation":
158                 case "xsd:simpleType": // only defines xsd:restriction with xsd:pattern/xsd:enumeration
159                 case "xsd:restriction":
160                 case "xsd:simpleContent": // bit of a late capture, followed "type" attribute and did not check what it was
161                 case "xsd:union":
162                 case "#text":
163                     // nothing, not even report ignoring, these are not relevant regarding element structure
164                     break;
165                 default:
166                     Logger.ots().trace("Ignoring a {}", child.getNodeName());
167             }
168         }
169     }
170 
171     /**
172      * Returns the element referred to by ref={ref} in an xsd:element. Will return {@code XiIncludeNode.XI_INCLUDE} for
173      * xi:include.
174      * @param node node, must have ref={ref} attribute.
175      * @param ref value of ref={ref}.
176      * @param schema schema to take element from.
177      * @return element referred to by ref={ref} in an xsd:element.
178      */
179     private static Node ref(final Node node, final String ref, final Schema schema)
180     {
181         if (ref.equals("xi:include"))
182         {
183             return XiIncludeNode.XI_INCLUDE;
184         }
185         return schema.getElement(ref)
186                 .orElseThrow(() -> new OtsRuntimeException("Unable to load ref for " + ref + " from XSD schema."));
187     }
188 
189     /**
190      * Returns a copy of the input list, with the extra node appended at the end.
191      * @param hiddenNodes hidden nodes list.
192      * @param node node to append.
193      * @return copy of the input list, with the extra node appended at the end.
194      */
195     private static ImmutableList<Node> append(final ImmutableList<Node> hiddenNodes, final Node node)
196     {
197         List<Node> list = new ArrayList<>(hiddenNodes.size() + 1);
198         list.addAll(hiddenNodes.toCollection());
199         list.add(node);
200         return new ImmutableArrayList<>(list, Immutable.WRAP);
201     }
202 
203     /**
204      * Returns the element referred to by type={type} in an xsd:element. Returns {@code null} all types starting with "xsd:" as
205      * these are standard types to which user input can be validated directly.
206      * @param node node, must have type={type} attribute.
207      * @param type value of type={type}.
208      * @param schema schema to take type from.
209      * @return element referred to by type={type} in an xsd:element or {@code null} for standard xsd types.
210      */
211     private static Node type(final Node node, final String type, final Schema schema)
212     {
213         if (type.startsWith("xsd:"))
214         {
215             return null;
216         }
217         return schema.getType(type)
218                 .orElseThrow(() -> new OtsRuntimeException("Unable to load type for " + type + " from XSD schema."));
219     }
220 
221     /**
222      * Returns a list of options derived from a list of restrictions (xsd:restriction) based on their internal xsd:enumeration.
223      * @param restrictions list of restrictions.
224      * @return list of options.
225      */
226     static List<String> getOptionsFromRestrictions(final List<Node> restrictions)
227     {
228         List<String> options = new ArrayList<>();
229         for (Node restriction : restrictions)
230         {
231             List<Node> enumerations = DocumentReader.getChildren(restriction, "xsd:enumeration");
232             for (Node enumeration : enumerations)
233             {
234                 options.add(DocumentReader.getAttribute(enumeration, "value").get());
235             }
236         }
237         return options;
238     }
239 
240     /**
241      * Recursively throws creation event on specific listener for all current nodes in the tree. This method is for
242      * {@code XsdTreeNodeRoot}.
243      * @param node node.
244      * @param listener listener.
245      * @throws RemoteException if event cannot be fired.
246      */
247     protected static void fireCreatedEventOnExistingNodes(final XsdTreeNode node, final EventListener listener)
248             throws RemoteException
249     {
250         List<XsdTreeNode> subNodes = node.children == null ? new ArrayList<>() : new ArrayList<>(node.children);
251         // only selected node extends towards choice, otherwise infinite recursion
252         if (node.choice != null && node.choice.selected.equals(node))
253         {
254             subNodes.add(node.choice);
255             subNodes.addAll(node.choice.options);
256             subNodes.remove(node);
257         }
258         for (XsdTreeNode child : subNodes)
259         {
260             fireCreatedEventOnExistingNodes(child, listener);
261         }
262         Event event = new Event(XsdTreeNodeRoot.NODE_CREATED, new Object[] {node, node.getParent(), subNodes.indexOf(node)});
263         listener.notify(event);
264     }
265 
266     /**
267      * Returns from the XSD definition the appropriate nodes to take children from at the level of the input node, in the order
268      * in which they should appear. This is often the xsd:complexType within an xsd:element, but can become as complex as
269      * containing multiple xsd:extension and their referred base types. An xsd:sequence is also common. Adding children in the
270      * order as they appear per {@code Node}, and in the order the {@code Node}'s are given, results in an overall order
271      * suitable for XML.
272      * @param node node to expand further.
273      * @param hiddenNodes nodes between the XSD node of the parent, and this tree node's XSD node.
274      * @param schema schema to retrieve types.
275      * @return map of nodes containing relevant children at the level of the input node, and their appropriate hidden nodes.
276      */
277     static Map<Node, ImmutableList<Node>> getRelevantNodesWithChildren(final Node node, final ImmutableList<Node> hiddenNodes,
278             final Schema schema)
279     {
280         Node complexType = node.getNodeName().equals("xsd:complexType") ? node
281                 : DocumentReader.getChild(node, "xsd:complexType").orElse(null);
282         if (complexType != null)
283         {
284             Optional<Node> sequence = DocumentReader.getChild(complexType, "xsd:sequence");
285             if (sequence.isPresent())
286             {
287                 return Map.of(sequence.get(), append(hiddenNodes, complexType));
288             }
289             Optional<Node> complexContent = DocumentReader.getChild(complexType, "xsd:complexContent");
290             if (complexContent.isPresent())
291             {
292                 Optional<Node> extension = DocumentReader.getChild(complexContent.get(), "xsd:extension");
293                 if (extension.isPresent())
294                 {
295                     ImmutableList<Node> hiddenExtension = append(append(hiddenNodes, complexType), complexContent.get());
296                     LinkedHashMap<Node, ImmutableList<Node>> elements = new LinkedHashMap<>();
297                     Optional<String> base = DocumentReader.getAttribute(extension.get(), "base");
298                     if (base.isPresent())
299                     {
300                         Optional<Node> baseNode = schema.getType(base.get());
301                         if (baseNode.isPresent())
302                         {
303                             elements.putAll(getRelevantNodesWithChildren(baseNode.get(),
304                                     append(hiddenExtension, extension.get()), schema));
305                         }
306                     }
307                     elements.put(extension.get(), hiddenExtension);
308                     return elements;
309                 }
310             }
311             return Map.of(complexType, hiddenNodes);
312         }
313         return Map.of(node, hiddenNodes);
314     }
315 
316     /**
317      * Returns whether nodes are of the same type. This regards the referring XSD node if it exists, otherwise it regards the
318      * regular XSD node.
319      * @param node1 node 1.
320      * @param node2 node 1.
321      * @return whether nodes are of the same type.
322      */
323     static boolean haveSameType(final XsdTreeNode node1, final XsdTreeNode node2)
324     {
325         return (node1.referringXsdNode != null && node1.referringXsdNode.equals(node2.referringXsdNode))
326                 || (node1.referringXsdNode == null && node1.xsdNode.equals(node2.xsdNode));
327     }
328 
329     /**
330      * Returns whether the two values are equal, where {@code null} is considered equal to an empty string.
331      * @param value1 value 1.
332      * @param value2 value 2.
333      * @return whether the two values are equal, where {@code null} is considered equal to an empty string.
334      */
335     public static boolean valuesAreEqual(final String value1, final String value2)
336     {
337         boolean value1Empty = value1 == null || value1.isEmpty();
338         boolean value2Empty = value2 == null || value2.isEmpty();
339         return (value1Empty && value2Empty) || (value1 != null && value1.equals(value2));
340     }
341 
342     /**
343      * Returns whether an xsdNode defines an editable element.
344      * @param xsdNode xsd node
345      * @param schema schema
346      * @return whether an xsdNode defines an editable element
347      */
348     static boolean isEditable(final Node xsdNode, final Schema schema)
349     {
350         if (xsdNode.equals(XiIncludeNode.XI_INCLUDE))
351         {
352             return false;
353         }
354         if (xsdNode.getChildNodes().getLength() == DocumentReader.getChildren(xsdNode, "#text").size()
355                 && xsdNode.getChildNodes().getLength() > 0)
356         {
357             // #text children only means a simple type
358             return true;
359         }
360         Node simpleType = xsdNode.getNodeName().equals("xsd:simpleType") ? xsdNode
361                 : DocumentReader.getChild(xsdNode, "xsd:simpleType").orElse(null);
362         if (simpleType != null)
363         {
364             return true;
365         }
366         Node complexType = xsdNode.getNodeName().equals("xsd:complexType") ? xsdNode
367                 : DocumentReader.getChild(xsdNode, "xsd:complexType").orElse(null);
368         boolean isComplex = complexType != null;
369         while (complexType != null)
370         {
371             Optional<Node> simpleContent = DocumentReader.getChild(complexType, "xsd:simpleContent");
372             if (simpleContent.isPresent())
373             {
374                 return true;
375             }
376             Optional<Node> complexContent = DocumentReader.getChild(complexType, "xsd:complexContent");
377             complexType = null;
378             if (complexContent.isPresent())
379             {
380                 Optional<Node> extension = DocumentReader.getChild(complexContent.get(), "xsd:extension");
381                 if (extension.isPresent())
382                 {
383                     String base = DocumentReader.getAttribute(extension.get(), "base").orElse(null);
384                     complexType = schema.getType(base).orElse(null);
385                 }
386             }
387         }
388         if (isComplex)
389         {
390             // complex and never found simpleContent through extension
391             return false;
392         }
393         Optional<String> type = DocumentReader.getAttribute(xsdNode, "type");
394         if (xsdNode.getNodeName().equals("xsd:element") && (type.isEmpty() || type.get().startsWith("xsd:")))
395         {
396             return true;
397         }
398         return false;
399     }
400 
401     /**
402      * Class that holds two indices related to loading XML nodes in to a structure of {@code XsdTreeNode}. Both pertain to the
403      * index in a list of child nodes.
404      */
405     static final class LoadingIndices
406     {
407         /** Index of XML node. */
408         private int xmlNode;
409 
410         /** Index of XsdTreeNode. */
411         private int xsdTreeNode;
412 
413         /**
414          * Constructor.
415          * @param xmlNode index of XML node
416          * @param xsdTreeNode index of XsdTreeNode
417          */
418         LoadingIndices(final int xmlNode, final int xsdTreeNode)
419         {
420             this.xmlNode = xmlNode;
421             this.xsdTreeNode = xsdTreeNode;
422         }
423 
424         /**
425          * Get XML node index.
426          * @return XML node index
427          */
428         public int getXmlNode()
429         {
430             return this.xmlNode;
431         }
432 
433         /**
434          * Set XML node index.
435          * @param xmlNode XML node index
436          */
437         public void setXmlNode(final int xmlNode)
438         {
439             this.xmlNode = xmlNode;
440         }
441 
442         /**
443          * Get XsdTreeNode index.
444          * @return XsdTreeNode index
445          */
446         public int getXsdTreeNode()
447         {
448             return this.xsdTreeNode;
449         }
450 
451         /**
452          * Set XsdTreeNode index.
453          * @param xsdTreeNode XsdTreeNode index
454          */
455         public void setXsdTreeNode(final int xsdTreeNode)
456         {
457             this.xsdTreeNode = xsdTreeNode;
458         }
459     }
460 
461     /**
462      * Enum to specify what occurs is requested.
463      */
464     enum Occurs
465     {
466         /** Value for minOccurs. */
467         MIN("minOccurs"),
468 
469         /** Value for maxOccurs. */
470         MAX("maxOccurs");
471 
472         /** Node attribute. */
473         private final String attribute;
474 
475         /**
476          * Constructor.
477          * @param attribute node attribute
478          */
479         Occurs(final String attribute)
480         {
481             this.attribute = attribute;
482         }
483 
484         /**
485          * Parses the minOcccurs or maxOccurs value from given node. If it is not supplied, the default of 1 is given.
486          * @param node node.
487          * @return value of occurs, -1 represents "unbounded".
488          */
489         public int get(final Node node)
490         {
491             Optional<String> occursValue = DocumentReader.getAttribute(node, this.attribute);
492             if (occursValue.isEmpty())
493             {
494                 return 1;
495             }
496             if ("unbounded".equals(occursValue.orElse(null)))
497             {
498                 return -1;
499             }
500             return Integer.valueOf(occursValue.get());
501         }
502     }
503 
504 }