View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2012-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.jcabi.xml;
6   
7   import com.jcabi.log.Logger;
8   import java.io.ByteArrayInputStream;
9   import java.io.File;
10  import java.io.IOException;
11  import java.nio.charset.StandardCharsets;
12  import javax.xml.parsers.DocumentBuilder;
13  import javax.xml.parsers.DocumentBuilderFactory;
14  import javax.xml.parsers.ParserConfigurationException;
15  import lombok.EqualsAndHashCode;
16  import lombok.ToString;
17  import org.w3c.dom.Document;
18  import org.xml.sax.SAXException;
19  
20  /**
21   * Convenient parser of XML to DOM.
22   *
23   * <p>Objects of this class are immutable and thread-safe.
24   *
25   * @since 0.1
26   */
27  @ToString
28  @EqualsAndHashCode
29  final class DomParser {
30  
31      /**
32       * Document builder factory to use for parsing.
33       */
34      private final transient DocumentBuilderFactory factory;
35  
36      /**
37       * Source of XML.
38       */
39      private final DocSource source;
40  
41      /**
42       * Public ctor.
43       *
44       * <p>An {@link IllegalArgumentException} may be thrown if the parameter
45       * passed is not in XML format. It doesn't perform a strict validation
46       * and is not guaranteed that an exception will be thrown whenever
47       * the parameter is not XML.
48       *
49       * <p>It is assumed that the text is in UTF-8.
50       *
51       * @param fct Document builder factory to use
52       * @param txt The XML in text (in UTF-8)
53       */
54      DomParser(final DocumentBuilderFactory fct, final String txt) {
55          this(fct, new BytesSource(txt));
56      }
57  
58      /**
59       * Public ctor.
60       *
61       * <p>An {@link IllegalArgumentException} may be thrown if the parameter
62       * passed is not in XML format. It doesn't perform a strict validation
63       * and is not guaranteed that an exception will be thrown whenever
64       * the parameter is not XML.
65       *
66       * @param fct Document builder factory to use
67       * @param bytes The XML in bytes
68       */
69      DomParser(final DocumentBuilderFactory fct, final byte[] bytes) {
70          this(fct, new BytesSource(bytes));
71      }
72  
73      /**
74       * Public ctor.
75       *
76       * <p>An {@link IllegalArgumentException} may be thrown if the parameter
77       * passed is not in XML format. It doesn't perform a strict validation
78       * and is not guaranteed that an exception will be thrown whenever
79       * the parameter is not XML.
80       *
81       * @param fct Document builder factory to use
82       * @param file The XML as a file
83       */
84      DomParser(final DocumentBuilderFactory fct, final File file) {
85          this(fct, new FileSource(file));
86      }
87  
88      /**
89       * Private ctor.
90       * @param factory Document builder factory to use
91       * @param source Source of XML
92       */
93      private DomParser(final DocumentBuilderFactory factory, final DocSource source) {
94          this.factory = factory;
95          this.source = source;
96      }
97  
98      /**
99       * Get the document body.
100      * @return The document
101      */
102     @SuppressWarnings("PMD.UnnecessaryLocalRule")
103     public Document document() {
104         final DocumentBuilder builder;
105         try {
106             builder = this.factory.newDocumentBuilder();
107         } catch (final ParserConfigurationException ex) {
108             throw new IllegalArgumentException(
109                 String.format(
110                     "Failed to create document builder by %s",
111                     this.factory.getClass().getName()
112                 ),
113                 ex
114             );
115         }
116         final long start = System.nanoTime();
117         final Document doc;
118         try {
119             doc = this.source.apply(builder);
120         } catch (final IOException | SAXException ex) {
121             throw new IllegalArgumentException(
122                 String.format(
123                     "Can't parse by %s, most probably the XML is invalid",
124                     builder.getClass().getName()
125                 ),
126                 ex
127             );
128         }
129         if (Logger.isTraceEnabled(this)) {
130             Logger.trace(
131                 this,
132                 "%s parsed %d bytes of XML in %[nano]s",
133                 builder.getClass().getName(),
134                 this.source.length(),
135                 System.nanoTime() - start
136             );
137         }
138         return doc;
139     }
140 
141     /**
142      * Source of XML.
143      * @since 0.32
144      */
145     private interface DocSource {
146 
147         /**
148          * Parse XML by the builder.
149          * @param builder The builder to use during parsing.
150          * @return The document.
151          * @throws IOException If fails.
152          * @throws SAXException If fails.
153          */
154         Document apply(DocumentBuilder builder) throws IOException, SAXException;
155 
156         /**
157          * The length of the source.
158          * @return The length.
159          */
160         long length();
161     }
162 
163     /**
164      * File source of XML from a file.
165      * @since 0.32
166      */
167     private static class FileSource implements DocSource {
168 
169         /**
170          * The file.
171          */
172         private final File file;
173 
174         /**
175          * Public ctor.
176          * @param file The file.
177          */
178         FileSource(final File file) {
179             this.file = file;
180         }
181 
182         @Override
183         public Document apply(final DocumentBuilder builder) throws IOException, SAXException {
184             return builder.parse(this.file);
185         }
186 
187         @Override
188         public long length() {
189             return this.file.length();
190         }
191     }
192 
193     /**
194      * Bytes source of XML.
195      * @since 0.32
196      */
197     private static class BytesSource implements DocSource {
198 
199         /**
200          * Bytes of the XML.
201          */
202         private final byte[] xml;
203 
204         /**
205          * Public ctor.
206          * @param xml Bytes of the XML.
207          */
208         BytesSource(final String xml) {
209             this(xml.getBytes(StandardCharsets.UTF_8));
210         }
211 
212         /**
213          * Public ctor.
214          * @param xml Bytes of the XML.
215          */
216         @SuppressWarnings("PMD.ArrayIsStoredDirectly")
217         BytesSource(final byte[] xml) {
218             this.xml = xml;
219         }
220 
221         @Override
222         public Document apply(final DocumentBuilder builder) throws IOException, SAXException {
223             return builder.parse(new ByteArrayInputStream(this.xml));
224         }
225 
226         @Override
227         public long length() {
228             return this.xml.length;
229         }
230     }
231 }