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 for regular expression to split string by upper case without disregarding the upper case itself. */
31      private static final Pattern UPPER_PATTERN = Pattern.compile("(?=\\p{Lu})");
32  
33      /** Validators for xsd:all nodes and their children. */
34      private final static 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 XsdTreeNode; shared xsd:all node.
47       * @param node XsdTreeNode; 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; node.
60       * @param attribute String; "minOccurs" or "maxOccurs".
61       * @return int; 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; node to get the children of.
80       * @param parentNode XsdTreeNode; parent node for the created children.
81       * @param children List&lt;XsdTreeNode&gt;; list to add the children to. This may be different from
82       *            {@code parentNode.children} due to layered choice structures.
83       * @param hiddenNodes ImmutableList&lt;Node&gt;; nodes between the XSD node of the parent, and this tree node's XSD node.
84       * @param schema XsdSchema; schema to get types and referred elements from.
85       * @param flattenSequence boolean; when true, treats an xsd:sequence child as an extension of the node. In the context of a
86       *            choice this should remain separated.
87       * @param skip int; 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 ImmutableList&lt;Node&gt;; hidden nodes list.
203      * @param node Node; node to append.
204      * @return ImmutableList&lt;Node&gt;; 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&lt;Node&gt;; list of restrictions.
217      * @return List&lt;String&gt;; 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 XsdTreeNode; node.
236      * @param listener EventListener; 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 int; previously determined insertion index; may be updated to lower value.
261      * @param removeIndex int; index of element that is removed.
262      * @return int; 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; node to expand further.
277      * @param hiddenNodes ImmutableList&lt;Node&gt;; nodes between the XSD node of the parent, and this tree node's XSD node.
278      * @param schema XsdSchema; schema to retrieve types.
279      * @return Map&lt;Node, ImmutableList&lt;Node&gt;&gt;; map of nodes containing relevant children at the level of the input
280      *         node, and their appropriate hidden nodes.
281      */
282     static Map<Node, ImmutableList<Node>> getRelevantNodesWithChildren(final Node node, final ImmutableList<Node> hiddenNodes,
283             final Schema schema)
284     {
285         Node complexType =
286                 node.getNodeName().equals("xsd:complexType") ? node : DocumentReader.getChild(node, "xsd:complexType");
287         if (complexType != null)
288         {
289             Node sequence = DocumentReader.getChild(complexType, "xsd:sequence");
290             if (sequence != null)
291             {
292                 return Map.of(sequence, append(hiddenNodes, complexType));
293             }
294             Node complexContent = DocumentReader.getChild(complexType, "xsd:complexContent");
295             if (complexContent != null)
296             {
297                 Node extension = DocumentReader.getChild(complexContent, "xsd:extension");
298                 if (extension != null)
299                 {
300                     ImmutableList<Node> hiddenExtension = append(append(hiddenNodes, complexType), complexContent);
301                     LinkedHashMap<Node, ImmutableList<Node>> elements = new LinkedHashMap<>();
302                     String base = DocumentReader.getAttribute(extension, "base");
303                     if (base != null)
304                     {
305                         Node baseNode = schema.getType(base);
306                         if (baseNode != null)
307                         {
308                             elements.putAll(getRelevantNodesWithChildren(baseNode, append(hiddenExtension, extension), schema));
309                         }
310                     }
311                     elements.put(extension, hiddenExtension);
312                     return elements;
313                 }
314             }
315             return Map.of(complexType, hiddenNodes);
316         }
317         return Map.of(node, hiddenNodes);
318     }
319 
320     /**
321      * Returns whether nodes are of the same type. This regards the referring XSD node if it exists, otherwise it regards the
322      * regular XSD node.
323      * @param node1 XsdTreeNode; node 1.
324      * @param node2 XsdTreeNode; node 1.
325      * @return boolean; whether nodes are of the same type.
326      */
327     static boolean haveSameType(final XsdTreeNode node1, final XsdTreeNode node2)
328     {
329         return (node1.referringXsdNode != null && node1.referringXsdNode.equals(node2.referringXsdNode))
330                 || (node1.referringXsdNode == null && node1.xsdNode.equals(node2.xsdNode));
331     }
332 
333     /**
334      * Returns the element referred to by ref={ref} in an xsd:element. Will return {@code XiIncludeNode.XI_INCLUDE} for
335      * xi:include.
336      * @param node Node; node, must have ref={ref} attribute.
337      * @param ref String; value of ref={ref}.
338      * @param schema XsdSchema; schema to take element from.
339      * @return Node; element referred to by ref={ref} in an xsd:element.
340      */
341     static Node ref(final Node node, final String ref, final Schema schema)
342     {
343         if (ref.equals("xi:include"))
344         {
345             return XiIncludeNode.XI_INCLUDE;
346         }
347         Node refNode = schema.getElement(ref);
348         Throw.when(refNode == null, RuntimeException.class, "Unable to load ref for %s from XSD schema.", ref);
349         return refNode;
350     }
351 
352     /**
353      * Returns the element referred to by type={type} in an xsd:element. Ignores all types starting with "xsd:" as these are
354      * standard types to which user input can be validated directly.
355      * @param node Node; node, must have type={type} attribute.
356      * @param type String; value of type={type}.
357      * @param schema XsdSchema; schema to take type from.
358      * @return Node; element referred to by type={type} in an xsd:element.
359      */
360     static Node type(final Node node, final String type, final Schema schema)
361     {
362         if (type.startsWith("xsd:"))
363         {
364             return null;
365         }
366         Node typeNode = schema.getType(type);
367         Throw.when(typeNode == null, RuntimeException.class, "Unable to load type for %s from XSD schema.", type);
368         return typeNode;
369     }
370 
371     /**
372      * Adds a thin space before each capital character in a {@code String}, except the first.
373      * @param name String; name of node.
374      * @return String; input string but with a thin space before each capital character, except the first.
375      */
376     static String separatedName(final String name)
377     {
378         String[] parts = UPPER_PATTERN.split(name);
379         if (parts.length == 1)
380         {
381             return parts[0];
382         }
383         String separator = "";
384         StringBuilder stringBuilder = new StringBuilder();
385         for (String part : parts)
386         {
387             stringBuilder.append(separator).append(part);
388             separator = " ";
389         }
390         return stringBuilder.toString();
391     }
392 
393     /**
394      * Returns whether the two values are equal, where {@code null} is consider equal to an empty string.
395      * @param value1 String; value 1.
396      * @param value2 String; value 2.
397      * @return whether the two values are equal, where {@code null} is consider equal to an empty string.
398      */
399     public static boolean valuesAreEqual(final String value1, final String value2)
400     {
401         boolean value1Empty = value1 == null || value1.isEmpty();
402         boolean value2Empty = value2 == null || value2.isEmpty();
403         return (value1Empty && value2Empty) || (value1 != null && value1.equals(value2));
404     }
405 
406 }