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 com.jcabi.log.Logger;
33  import java.io.File;
34  import java.io.FileNotFoundException;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.io.StringReader;
38  import java.net.URI;
39  import java.net.URL;
40  import java.nio.file.Path;
41  import java.util.Collection;
42  import java.util.concurrent.CopyOnWriteArrayList;
43  import javax.xml.XMLConstants;
44  import javax.xml.transform.Source;
45  import javax.xml.transform.stream.StreamSource;
46  import javax.xml.validation.Schema;
47  import javax.xml.validation.SchemaFactory;
48  import javax.xml.validation.Validator;
49  import lombok.EqualsAndHashCode;
50  import org.xml.sax.ErrorHandler;
51  import org.xml.sax.SAXException;
52  import org.xml.sax.SAXParseException;
53  
54  /**
55   * Implementation of {@link XSD}.
56   *
57   * <p>Objects of this class are immutable and thread-safe.
58   *
59   * @since 0.5
60   * @checkstyle AbbreviationAsWordInNameCheck (5 lines)
61   */
62  @EqualsAndHashCode(of = "xsd")
63  public final class XSDDocument implements XSD {
64  
65      /**
66       * XSD document.
67       */
68      private final transient String xsd;
69  
70      /**
71       * Public ctor, from XSD as a source.
72       * @param src XSD document body
73       */
74      public XSDDocument(final XML src) {
75          this(src.toString());
76      }
77  
78      /**
79       * Public ctor, from XSD as a string.
80       * @param src XSD document body
81       */
82      public XSDDocument(final String src) {
83          this.xsd = src;
84      }
85  
86      /**
87       * Public ctor, from URL.
88       * @param url Location of document
89       * @throws IOException If fails to read
90       * @since 0.7.4
91       */
92      public XSDDocument(final URL url) throws IOException {
93          this(new TextResource(url).toString());
94      }
95  
96      /**
97       * Public ctor, from file.
98       * @param file Location of document
99       * @throws FileNotFoundException If fails to read
100      * @since 0.21
101      */
102     public XSDDocument(final Path file) throws FileNotFoundException {
103         this(file.toFile());
104     }
105 
106     /**
107      * Public ctor, from file.
108      * @param file Location of document
109      * @throws FileNotFoundException If fails to read
110      * @since 0.21
111      */
112     public XSDDocument(final File file) throws FileNotFoundException {
113         this(new TextResource(file).toString());
114     }
115 
116     /**
117      * Public ctor, from URI.
118      * @param uri Location of document
119      * @throws IOException If fails to read
120      * @since 0.15
121      */
122     public XSDDocument(final URI uri) throws IOException {
123         this(new TextResource(uri).toString());
124     }
125 
126     /**
127      * Public ctor, from XSD as an input stream.
128      * @param stream XSD input stream
129      */
130     public XSDDocument(final InputStream stream) {
131         this(new TextResource(stream).toString());
132     }
133 
134     /**
135      * Make an instance of XSD schema without I/O exceptions.
136      *
137      * <p>This factory method is useful when you need to create
138      * an instance of XSD schema as a static final variable. In this
139      * case you can't catch an exception but this method can help, for example:
140      *
141      * <pre> class Foo {
142      *   private static final XSD SCHEMA = XSDDocument.make(
143      *     Foo.class.getResourceAsStream("my-schema.xsd")
144      *   );
145      * }</pre>
146      *
147      * @param stream Input stream
148      * @return XSD schema
149      */
150     @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
151     public static XSD make(final InputStream stream) {
152         return new XSDDocument(stream);
153     }
154 
155     /**
156      * Make an instance of XSD schema without I/O exceptions.
157      * @param url URL with content
158      * @return XSD schema
159      * @see #make(InputStream)
160      * @since 0.7.4
161      */
162     @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
163     public static XSD make(final URL url) {
164         try {
165             return new XSDDocument(url);
166         } catch (final IOException ex) {
167             throw new IllegalStateException(ex);
168         }
169     }
170 
171     @Override
172     public String toString() {
173         return new XMLDocument(this.xsd).toString();
174     }
175 
176     @Override
177     public Collection<SAXParseException> validate(final Source xml) {
178         final Schema schema;
179         try {
180             synchronized (XSDDocument.class) {
181                 schema = SchemaFactory
182                     .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
183                     .newSchema(new StreamSource(new StringReader(this.xsd)));
184             }
185         } catch (final SAXException ex) {
186             throw new IllegalStateException(
187                 String.format("Failed to create XSD schema from %s", this.xsd),
188                 ex
189             );
190         }
191         final Collection<SAXParseException> errors =
192             new CopyOnWriteArrayList<>();
193         final Validator validator = schema.newValidator();
194         validator.setErrorHandler(new XSDDocument.ValidationHandler(errors));
195         try {
196             synchronized (XSDDocument.class) {
197                 validator.validate(xml);
198             }
199         } catch (final SAXException | IOException ex) {
200             throw new IllegalStateException(ex);
201         }
202         if (Logger.isDebugEnabled(this)) {
203             Logger.debug(
204                 this, "%s detected %d error(s)",
205                 schema.getClass().getName(), errors.size()
206             );
207         }
208         return errors;
209     }
210 
211     /**
212      * Validation error handler.
213      *
214      * @since 0.1
215      */
216     static final class ValidationHandler implements ErrorHandler {
217         /**
218          * Errors.
219          */
220         private final transient Collection<SAXParseException> errors;
221 
222         /**
223          * Constructor.
224          * @param errs Collection of errors
225          */
226         ValidationHandler(final Collection<SAXParseException> errs) {
227             this.errors = errs;
228         }
229 
230         @Override
231         public void warning(final SAXParseException error) {
232             this.errors.add(error);
233         }
234 
235         @Override
236         public void error(final SAXParseException error) {
237             this.errors.add(error);
238         }
239 
240         @Override
241         public void fatalError(final SAXParseException error) {
242             this.errors.add(error);
243         }
244     }
245 
246 }