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 }