View Javadoc
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() &amp;&amp; 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          * &lt;xsd:sequence&gt;
134          *   &lt;xsd:annotation&gt;
135          *     &lt;xsd:description&gt;describes the sequence&lt;/xsd:description&gt;
136          *     &lt;xsd:appinfo&gt;
137          *       &lt;ots:name&gt;annotates the sequence&lt;/ots:name&gt;
138          *     &lt;/xsd:appinfo&gt;
139          *   &lt;/xsd:annotation&gt;
140          * &lt;/xsd:sequence&gt;
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          * &lt;xsd:sequence&gt;
180          *   &lt;xsd:annotation&gt;
181          *     &lt;xsd:description&gt;describes the sequence&lt;/xsd:description&gt;
182          *     &lt;xsd:appinfo&gt;
183          *       &lt;ots:name&gt;annotates the sequence&lt;/ots:name&gt;
184          *     &lt;/xsd:appinfo&gt;
185          *   &lt;/xsd:annotation&gt;
186          * &lt;/xsd:sequence&gt;
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 }