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