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 }