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<XsdTreeNode>; list to add the children to. This may be different from
82 * {@code parentNode.children} due to layered choice structures.
83 * @param hiddenNodes ImmutableList<Node>; 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<Node>; hidden nodes list.
203 * @param node Node; node to append.
204 * @return ImmutableList<Node>; 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<Node>; list of restrictions.
217 * @return List<String>; 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<Node>; 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<Node, ImmutableList<Node>>; 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 }