View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2012-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.jcabi.xml;
6   
7   import java.io.File;
8   import java.io.IOException;
9   import java.io.InputStream;
10  import java.io.StringReader;
11  import java.net.URI;
12  import java.net.URL;
13  import java.nio.charset.StandardCharsets;
14  import java.nio.file.Path;
15  import java.util.Collection;
16  import java.util.List;
17  import java.util.stream.Collectors;
18  import javax.xml.namespace.NamespaceContext;
19  import javax.xml.transform.stream.StreamSource;
20  import net.sf.saxon.s9api.DocumentBuilder;
21  import net.sf.saxon.s9api.Processor;
22  import net.sf.saxon.s9api.SaxonApiException;
23  import net.sf.saxon.s9api.XPathCompiler;
24  import net.sf.saxon.s9api.XPathSelector;
25  import net.sf.saxon.s9api.XdmItem;
26  import net.sf.saxon.s9api.XdmNode;
27  import org.w3c.dom.Node;
28  import org.w3c.dom.ls.LSResourceResolver;
29  import org.xml.sax.SAXParseException;
30  
31  /**
32   * Saxon XML document.
33   *
34   * <p>Objects of this class are immutable, but NOT thread-safe.
35   *
36   * @since 0.28
37   */
38  @SuppressWarnings("PMD.TooManyMethods")
39  public final class SaxonDocument implements XML {
40  
41      /**
42       * Saxon processor.
43       */
44      private static final Processor SAXON = new Processor(false);
45  
46      /**
47       * Saxon document builder.
48       */
49      private static final DocumentBuilder DOC_BUILDER = SaxonDocument.SAXON.newDocumentBuilder();
50  
51      /**
52       * Saxon XPath compiler.
53       */
54      private static final XPathCompiler XPATH_COMPILER = SaxonDocument.SAXON.newXPathCompiler();
55  
56      /**
57       * Exception message for unsupported methods.
58       */
59      private static final String UNSUPPORTED =
60          "The %s method is not supported yet. You can use XMLDocument instead or if you need to use Saxon specific features, you can open an issue at https://github.com/jcabi/jcabi-xml";
61  
62      /**
63       * Saxon XML document node.
64       */
65      private final XdmNode xdm;
66  
67      /**
68       * Public constructor from XML as string text.
69       * @param text XML document body.
70       * @since 0.28.0
71       */
72      public SaxonDocument(final String text) {
73          this(SaxonDocument.node(text));
74      }
75  
76      /**
77       * Public constructor from XML as byte array.
78       * @param data XML document body as byte array.
79       * @since 0.28.1
80       */
81      public SaxonDocument(final byte[] data) {
82          this(SaxonDocument.node(new String(data, StandardCharsets.UTF_8)));
83      }
84  
85      /**
86       * Public constructor from XML saved in a filesystem.
87       * @param path Path to XML file in a filesystem.
88       * @since 0.28.1
89       */
90      public SaxonDocument(final Path path) {
91          this(path.toFile());
92      }
93  
94      /**
95       * Public constructor from XML saved in a filesystem.
96       * @param file XML file in a filesystem.
97       * @since 0.28.1
98       */
99      public SaxonDocument(final File file) {
100         this(SaxonDocument.node(new StreamSource(file)));
101     }
102 
103     /**
104      * Public constructor from XML reached by URL.
105      * @param url URL of XML document.
106      * @throws IOException If fails.
107      * @since 0.28.1
108      */
109     public SaxonDocument(final URL url) throws IOException {
110         this(SaxonDocument.node(new TextResource(url).toString()));
111     }
112 
113     /**
114      * Public constructor from XML reached by URI.
115      * @param uri URI of XML document.
116      * @throws IOException If fails.
117      * @since 0.28.1
118      */
119     public SaxonDocument(final URI uri) throws IOException {
120         this(SaxonDocument.node(new TextResource(uri).toString()));
121     }
122 
123     /**
124      * Public constructor from XML as input stream.
125      * @param stream Input stream with XML document.
126      * @since 0.28.1
127      */
128     public SaxonDocument(final InputStream stream) {
129         this(SaxonDocument.node(new StreamSource(stream)));
130     }
131 
132     /**
133      * Public constructor from Saxon XML document node.
134      * @param xml Saxon XML document node.
135      * @since 0.28.0
136      */
137     public SaxonDocument(final XdmNode xml) {
138         this.xdm = xml;
139     }
140 
141     @Override
142     public List<String> xpath(final String query) {
143         try {
144             final XPathSelector selector = SaxonDocument.XPATH_COMPILER.compile(query).load();
145             selector.setContextItem(this.xdm);
146             return selector.evaluate()
147                 .stream()
148                 .map(XdmItem::getStringValue)
149                 .collect(Collectors.toList());
150         } catch (final SaxonApiException exception) {
151             throw new IllegalArgumentException(
152                 String.format("Can't evaluate the '%s' XPath query with Saxon API", query),
153                 exception
154             );
155         }
156     }
157 
158     @Override
159     public List<XML> nodes(final String query) {
160         throw new UnsupportedOperationException(
161             String.format(SaxonDocument.UNSUPPORTED, "nodes")
162         );
163     }
164 
165     @Override
166     public XML registerNs(final String prefix, final Object uri) {
167         throw new UnsupportedOperationException(
168             String.format(SaxonDocument.UNSUPPORTED, "registerNs")
169         );
170     }
171 
172     @Override
173     public XML merge(final NamespaceContext context) {
174         throw new UnsupportedOperationException(
175             String.format(SaxonDocument.UNSUPPORTED, "merge")
176         );
177     }
178 
179     /**
180      * Retrieve DOM node, represented by this wrapper.
181      * This method works exactly the same as {@link #deepCopy()}.
182      * @return Deep copy of the inner DOM node.
183      * @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
184      */
185     @Deprecated
186     public Node node() {
187         throw new UnsupportedOperationException(
188             String.format(SaxonDocument.UNSUPPORTED, "node")
189         );
190     }
191 
192     @Override
193     public Node inner() {
194         throw new UnsupportedOperationException(
195             String.format(SaxonDocument.UNSUPPORTED, "inner")
196         );
197     }
198 
199     @Override
200     public Node deepCopy() {
201         throw new UnsupportedOperationException(
202             String.format(SaxonDocument.UNSUPPORTED, "deepCopy")
203         );
204     }
205 
206     @Override
207     public Collection<SAXParseException> validate(final LSResourceResolver resolver) {
208         throw new UnsupportedOperationException(
209             String.format(SaxonDocument.UNSUPPORTED, "validate")
210         );
211     }
212 
213     @Override
214     public Collection<SAXParseException> validate(final XML xsd) {
215         throw new UnsupportedOperationException(
216             String.format(SaxonDocument.UNSUPPORTED, "validate")
217         );
218     }
219 
220     /**
221      * Build Saxon XML document node from XML string text.
222      * @param text XML string text.
223      * @return Saxon XML document node.
224      */
225     private static XdmNode node(final String text) {
226         return SaxonDocument.node(new StreamSource(new StringReader(text)));
227     }
228 
229     /**
230      * Build Saxon XML document node from XML source.
231      * @param source XML.
232      * @return Saxon XML document node.
233      */
234     private static XdmNode node(final StreamSource source) {
235         try {
236             return SaxonDocument.DOC_BUILDER.build(source);
237         } catch (final SaxonApiException exception) {
238             throw new IllegalArgumentException(
239                 String.format("SaxonDocument can't parse XML from source '%s'", source),
240                 exception
241             );
242         }
243     }
244 }