1 package org.opentrafficsim.editor;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.URI;
6 import java.util.ArrayList;
7 import java.util.List;
8 import java.util.Optional;
9
10 import javax.xml.parsers.DocumentBuilder;
11 import javax.xml.parsers.DocumentBuilderFactory;
12 import javax.xml.parsers.ParserConfigurationException;
13
14 import org.w3c.dom.Document;
15 import org.w3c.dom.Node;
16 import org.xml.sax.SAXException;
17
18 /**
19 * Utility class to read XSD or XML from URI. There are also methods to obtain certain information from a node.
20 * <p>
21 * Copyright (c) 2023-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
22 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
23 * </p>
24 * @author Wouter Schakel
25 */
26 public final class DocumentReader
27 {
28
29 /**
30 * Private constructor.
31 */
32 private DocumentReader()
33 {
34
35 }
36
37 /**
38 * Opens an XSD or XML file.
39 * @param file file.
40 * @return document, i.e. the root of the XSD file.
41 * @throws SAXException exception
42 * @throws IOException exception
43 * @throws ParserConfigurationException exception
44 */
45 public static Document open(final URI file) throws SAXException, IOException, ParserConfigurationException
46 {
47 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
48 // dbf.setXIncludeAware(true);
49 dbf.setIgnoringComments(true);
50 dbf.setIgnoringElementContentWhitespace(true);
51 DocumentBuilder db = dbf.newDocumentBuilder();
52 Document doc = db.parse(new File(file));
53 return doc;
54 }
55
56 /**
57 * Returns the attribute of a node. This is short for:
58 *
59 * <pre>
60 * Optional.ofNullable(node.hasAttributes() && node.getAttributes().getNamedItem(name) != null
61 * ? node.getAttributes().getNamedItem(name).getNodeValue() : null);
62 * </pre>
63 *
64 * @param node node.
65 * @param name attribute name.
66 * @return value of the attribute in the node.
67 */
68 public static Optional<String> getAttribute(final Node node, final String name)
69 {
70 return Optional.ofNullable(node.hasAttributes() && node.getAttributes().getNamedItem(name) != null
71 ? node.getAttributes().getNamedItem(name).getNodeValue() : null);
72 }
73
74 /**
75 * Returns a child node of specified type. It should be a type of which there may be only one.
76 * @param node node
77 * @param type child type, e.g. xsd:complexType.
78 * @return child node of specified type, empty if no such child.
79 */
80 public static Optional<Node> getChild(final Node node, final String type)
81 {
82 if (node.hasChildNodes())
83 {
84 for (int childIndex = 0; childIndex < node.getChildNodes().getLength(); childIndex++)
85 {
86 Node child = node.getChildNodes().item(childIndex);
87 if (child.getNodeName().equals(type))
88 {
89 return Optional.of(child);
90 }
91 }
92 }
93 return Optional.empty();
94 }
95
96 /**
97 * Returns child nodes of specified type.
98 * @param node node
99 * @param type child type, e.g. xsd:field.
100 * @return child nodes of specified type, empty {@code List} of no such child.
101 */
102 public static List<Node> getChildren(final Node node, final String type)
103 {
104 ArrayList<Node> children = new ArrayList<>();
105 if (node.hasChildNodes())
106 {
107 for (int childIndex = 0; childIndex < node.getChildNodes().getLength(); childIndex++)
108 {
109 Node child = node.getChildNodes().item(childIndex);
110 if (child.getNodeName().equals(type))
111 {
112 children.add(child);
113 }
114 }
115 }
116 return children;
117 }
118
119 /**
120 * Remove HTML tags from string.
121 * @param string input string
122 * @return string with HTML tags removed, or {@code null} if the input is {@code null}
123 */
124 public static String filterHtml(final String string)
125 {
126 return string == null ? null : string.replaceAll("\\<[^>]*>", "");
127 }
128
129 /**
130 * Types of annotation elements the {@code DocumentReader} can read. These are defined as below.
131 *
132 * <pre>
133 * <xsd:sequence>
134 * <xsd:annotation>
135 * <xsd:description>describes the sequence</xsd:description>
136 * <xsd:appinfo>
137 * <ots:name>annotates the sequence</ots:name>
138 * </xsd:appinfo>
139 * </xsd:annotation>
140 * </xsd:sequence>
141 * </pre>
142 */
143 public enum NodeAnnotation
144 {
145 /** Element xsd:documentation. */
146 DESCRIPTION("xsd:documentation", null),
147
148 /** Element xsd:appinfo/ots:name. */
149 APPINFO_NAME("xsd:appinfo", "ots:name"),
150
151 /** Element xsd:appinfo/ots:pattern. */
152 APPINFO_PATTERN("xsd:appinfo", "ots:pattern"),
153
154 /** Element xsd:appinfo/ots:defaultValue. */
155 APPINFO_DEFAULT_VALUE("xsd:appinfo", "ots:defaultValue");
156
157 /** Element name. */
158 private final String annotationName;
159
160 /** Tag. */
161 private final String tag;
162
163 /**
164 * Constructor.
165 * @param annotationName annotation name
166 * @param tag tag name
167 */
168 NodeAnnotation(final String annotationName, final String tag)
169 {
170 this.annotationName = annotationName;
171 this.tag = tag;
172 }
173
174 /**
175 * Returns an annotation value. These are defined as below. All space-like characters are replaced by blanks, and
176 * consecutive blanks are removed.
177 *
178 * <pre>
179 * <xsd:sequence>
180 * <xsd:annotation>
181 * <xsd:description>describes the sequence</xsd:description>
182 * <xsd:appinfo>
183 * <ots:name>annotates the sequence</ots:name>
184 * </xsd:appinfo>
185 * </xsd:annotation>
186 * </xsd:sequence>
187 * </pre>
188 *
189 * @param node node, either xsd:element or xsd:attribute.
190 * @return annotation value, empty if not found.
191 */
192 public Optional<String> get(final Node node)
193 {
194 for (Node child : DocumentReader.getChildren(node, "xsd:annotation"))
195 {
196 for (Node annotation : DocumentReader.getChildren(child, this.annotationName))
197 {
198 if (this.annotationName.equals(annotation.getNodeName()))
199 {
200 if (this.tag == null)
201 {
202 return getNodeValue(annotation);
203 }
204 for (Node tagChild : DocumentReader.getChildren(annotation, this.tag))
205 {
206 // As in Highlander, there can be only one.
207 return getNodeValue(tagChild);
208 }
209 }
210 }
211 }
212 return Optional.empty();
213 }
214
215 /**
216 * Returns the combined textual content of the node.
217 * @param annotation node
218 * @return combined textual content
219 */
220 private Optional<String> getNodeValue(final Node annotation)
221 {
222 StringBuilder str = new StringBuilder();
223 for (int appIndex = 0; appIndex < annotation.getChildNodes().getLength(); appIndex++)
224 {
225 Node appInfo = annotation.getChildNodes().item(appIndex);
226 if (appInfo.getNodeName().equals("#text") || appInfo.getNodeName().equals("#cdata-section"))
227 {
228 str.append(appInfo.getNodeValue());
229 }
230 }
231 // tabs, line break, etc. to blanks, then remove consecutive blanks, then trailing/leading blanks
232 return Optional.of(str.toString().replaceAll("\\s", " ").replaceAll("\\s{2,}", " ").trim());
233 }
234 }
235
236 }