View Javadoc
1   /*
2    * Copyright (c) 2012-2022, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.xml;
31  
32  import java.io.File;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.StringReader;
36  import java.net.URI;
37  import java.net.URL;
38  import java.nio.charset.StandardCharsets;
39  import java.nio.file.Path;
40  import java.util.List;
41  import java.util.stream.Collectors;
42  import javax.xml.namespace.NamespaceContext;
43  import javax.xml.transform.stream.StreamSource;
44  import net.sf.saxon.s9api.DocumentBuilder;
45  import net.sf.saxon.s9api.Processor;
46  import net.sf.saxon.s9api.SaxonApiException;
47  import net.sf.saxon.s9api.XPathCompiler;
48  import net.sf.saxon.s9api.XPathSelector;
49  import net.sf.saxon.s9api.XdmItem;
50  import net.sf.saxon.s9api.XdmNode;
51  import org.w3c.dom.Node;
52  
53  /**
54   * Saxon XML document.
55   *
56   * <p>Objects of this class are immutable, but NOT thread-safe.
57   *
58   * @since 0.28
59   */
60  public final class SaxonDocument implements XML {
61  
62      /**
63       * Saxon processor.
64       */
65      private static final Processor SAXON = new Processor(false);
66  
67      /**
68       * Saxon document builder.
69       */
70      private static final DocumentBuilder DOC_BUILDER = SaxonDocument.SAXON.newDocumentBuilder();
71  
72      /**
73       * Saxon XPath compiler.
74       */
75      private static final XPathCompiler XPATH_COMPILER = SaxonDocument.SAXON.newXPathCompiler();
76  
77      /**
78       * Exception message for unsupported methods.
79       */
80      private static final String UNSUPPORTED =
81          "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";
82  
83      /**
84       * Saxon XML document node.
85       */
86      private final XdmNode xdm;
87  
88      /**
89       * Public constructor from XML as string text.
90       * @param text XML document body.
91       * @since 0.28.0
92       */
93      public SaxonDocument(final String text) {
94          this(SaxonDocument.node(text));
95      }
96  
97      /**
98       * Public constructor from XML as byte array.
99       * @param data XML document body as byte array.
100      * @since 0.28.1
101      */
102     public SaxonDocument(final byte[] data) {
103         this(SaxonDocument.node(new String(data, StandardCharsets.UTF_8)));
104     }
105 
106     /**
107      * Public constructor from XML saved in a filesystem.
108      * @param path Path to XML file in a filesystem.
109      * @since 0.28.1
110      */
111     public SaxonDocument(final Path path) {
112         this(path.toFile());
113     }
114 
115     /**
116      * Public constructor from XML saved in a filesystem.
117      * @param file XML file in a filesystem.
118      * @since 0.28.1
119      */
120     public SaxonDocument(final File file) {
121         this(SaxonDocument.node(new StreamSource(file)));
122     }
123 
124     /**
125      * Public constructor from XML reached by URL.
126      * @param url URL of XML document.
127      * @throws IOException If fails.
128      * @since 0.28.1
129      */
130     public SaxonDocument(final URL url) throws IOException {
131         this(SaxonDocument.node(new TextResource(url).toString()));
132     }
133 
134     /**
135      * Public constructor from XML reached by URI.
136      * @param uri URI of XML document.
137      * @throws IOException If fails.
138      * @since 0.28.1
139      */
140     public SaxonDocument(final URI uri) throws IOException {
141         this(SaxonDocument.node(new TextResource(uri).toString()));
142     }
143 
144     /**
145      * Public constructor from XML as input stream.
146      * @param stream Input stream with XML document.
147      * @since 0.28.1
148      */
149     public SaxonDocument(final InputStream stream) {
150         this(SaxonDocument.node(new StreamSource(stream)));
151     }
152 
153     /**
154      * Public constructor from Saxon XML document node.
155      * @param xml Saxon XML document node.
156      * @since 0.28.0
157      */
158     public SaxonDocument(final XdmNode xml) {
159         this.xdm = xml;
160     }
161 
162     @Override
163     public List<String> xpath(final String query) {
164         try {
165             final XPathSelector selector = SaxonDocument.XPATH_COMPILER.compile(query).load();
166             selector.setContextItem(this.xdm);
167             return selector.evaluate()
168                 .stream()
169                 .map(XdmItem::getStringValue)
170                 .collect(Collectors.toList());
171         } catch (final SaxonApiException exception) {
172             throw new IllegalArgumentException(
173                 String.format("Can't evaluate the '%s' XPath query with Saxon API", query),
174                 exception
175             );
176         }
177     }
178 
179     @Override
180     public List<XML> nodes(final String query) {
181         throw new UnsupportedOperationException(
182             String.format(SaxonDocument.UNSUPPORTED, "nodes")
183         );
184     }
185 
186     @Override
187     public XML registerNs(final String prefix, final Object uri) {
188         throw new UnsupportedOperationException(
189             String.format(SaxonDocument.UNSUPPORTED, "registerNs")
190         );
191     }
192 
193     @Override
194     public XML merge(final NamespaceContext context) {
195         throw new UnsupportedOperationException(
196             String.format(SaxonDocument.UNSUPPORTED, "merge")
197         );
198     }
199 
200     @Override
201     public Node node() {
202         throw new UnsupportedOperationException(
203             String.format(SaxonDocument.UNSUPPORTED, "node")
204         );
205     }
206 
207     /**
208      * Build Saxon XML document node from XML string text.
209      * @param text XML string text.
210      * @return Saxon XML document node.
211      */
212     private static XdmNode node(final String text) {
213         return SaxonDocument.node(new StreamSource(new StringReader(text)));
214     }
215 
216     /**
217      * Build Saxon XML document node from XML source.
218      * @param source XML.
219      * @return Saxon XML document node.
220      */
221     private static XdmNode node(final StreamSource source) {
222         try {
223             return SaxonDocument.DOC_BUILDER.build(source);
224         } catch (final SaxonApiException exception) {
225             throw new IllegalArgumentException(
226                 String.format("SaxonDocument can't parse XML from source '%s'", source),
227                 exception
228             );
229         }
230     }
231 }