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.IOException;
34  import java.net.SocketException;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.Iterator;
38  import java.util.List;
39  import java.util.concurrent.CopyOnWriteArrayList;
40  import javax.xml.XMLConstants;
41  import javax.xml.namespace.NamespaceContext;
42  import javax.xml.transform.dom.DOMSource;
43  import javax.xml.validation.SchemaFactory;
44  import javax.xml.validation.Validator;
45  import lombok.EqualsAndHashCode;
46  import org.w3c.dom.Node;
47  import org.w3c.dom.ls.LSResourceResolver;
48  import org.xml.sax.SAXException;
49  import org.xml.sax.SAXParseException;
50  
51  /**
52   * Strict {@link XML} that fails if encapsulated XML document
53   * doesn't validate against externally provided XSD schema or internally
54   * specified schema locations.
55   *
56   * <p>Objects of this class are immutable and thread-safe.
57   *
58   * @since 0.7
59   * @checkstyle AbbreviationAsWordInNameCheck (5 lines)
60   */
61  @EqualsAndHashCode(of = "origin")
62  public final class StrictXML implements XML {
63  
64      /**
65       * Original XML document.
66       */
67      private final transient XML origin;
68  
69      /**
70       * Public ctor.
71       * @param xml XML document
72       */
73      public StrictXML(final XML xml) {
74          this(xml, new ClasspathResolver());
75      }
76  
77      /**
78       * Public ctor.
79       * @param xml XML document
80       * @param resolver Custom resolver
81       * @since 0.19
82       */
83      public StrictXML(final XML xml, final LSResourceResolver resolver) {
84          this(xml, StrictXML.newValidator(resolver));
85      }
86  
87      /**
88       * Public ctor.
89       * @param xml XML document
90       * @param val Custom validator
91       */
92      public StrictXML(final XML xml, final Validator val) {
93          this(xml, StrictXML.validate(xml, val));
94      }
95  
96      /**
97       * Public ctor.
98       * @param xml XML document
99       * @param schema XSD schema
100      */
101     public StrictXML(final XML xml, final XSD schema) {
102         this(xml, schema.validate(new DOMSource(xml.node())));
103     }
104 
105     /**
106      * Private ctor.
107      * @param xml XML Document
108      * @param errors XML Document errors
109      */
110     @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
111     private StrictXML(final XML xml,
112         final Collection<SAXParseException> errors) {
113         if (!errors.isEmpty()) {
114             Logger.warn(
115                 StrictXML.class,
116                 "%d XML validation error(s):\n  %s\n%s",
117                 errors.size(),
118                 StrictXML.join(StrictXML.print(errors), "\n  "),
119                 xml
120             );
121             throw new IllegalArgumentException(
122                 String.format(
123                     "%d error(s) in XML document: %s",
124                     errors.size(),
125                     StrictXML.join(StrictXML.print(errors), ";")
126                 )
127             );
128         }
129         this.origin = xml;
130     }
131 
132     @Override
133     public String toString() {
134         return this.origin.toString();
135     }
136 
137     @Override
138     public List<String> xpath(final String query) {
139         return this.origin.xpath(query);
140     }
141 
142     @Override
143     public List<XML> nodes(final String query) {
144         return this.origin.nodes(query);
145     }
146 
147     @Override
148     public XML registerNs(final String prefix, final Object uri) {
149         return this.origin.registerNs(prefix, uri);
150     }
151 
152     @Override
153     public XML merge(final NamespaceContext context) {
154         return this.origin.merge(context);
155     }
156 
157     @Override
158     public Node node() {
159         return this.origin.node();
160     }
161 
162     /**
163      * Convert errors to lines.
164      * @param errors The errors
165      * @return List of messages to print
166      */
167     private static Iterable<String> print(
168         final Collection<SAXParseException> errors) {
169         final Collection<String> lines = new ArrayList<>(errors.size());
170         for (final SAXParseException error : errors) {
171             lines.add(
172                 String.format(
173                     "%d:%d: %s",
174                     error.getLineNumber(),
175                     error.getColumnNumber(),
176                     error.getMessage()
177                 )
178             );
179         }
180         return lines;
181     }
182 
183     /**
184      * Joins many objects' string representations with the given separator
185      * string. The separator will not be appended to the beginning or the end.
186      * @param iterable Iterable of objects.
187      * @param sep Separator string.
188      * @return Joined string.
189      */
190     private static String join(final Iterable<?> iterable, final String sep) {
191         final Iterator<?> iterator = iterable.iterator();
192         final Object first = iterator.next();
193         final StringBuilder buf = new StringBuilder(256);
194         if (first != null) {
195             buf.append(first);
196         }
197         while (iterator.hasNext()) {
198             buf.append(sep);
199             final Object obj = iterator.next();
200             if (obj != null) {
201                 buf.append(obj);
202             }
203         }
204         return buf.toString();
205     }
206 
207     /**
208      * Validate XML without external schema.
209      * @param xml XML Document
210      * @param validator XML Validator
211      * @return List of validation errors
212      */
213     private static Collection<SAXParseException> validate(
214         final XML xml,
215         final Validator validator) {
216         final Collection<SAXParseException> errors =
217             new CopyOnWriteArrayList<>();
218         final int max = 3;
219         try {
220             validator.setErrorHandler(
221                 new XSDDocument.ValidationHandler(errors)
222             );
223             final DOMSource dom = new DOMSource(xml.node());
224             for (int retry = 1; retry <= max; ++retry) {
225                 try {
226                     validator.validate(dom);
227                     break;
228                 } catch (final SocketException ex) {
229                     Logger.error(
230                         StrictXML.class,
231                         "Try #%d of %d failed: %s: %s",
232                         retry,
233                         max,
234                         ex.getClass().getName(),
235                         ex.getMessage()
236                     );
237                     if (retry == max) {
238                         throw new IllegalStateException(ex);
239                     }
240                 }
241             }
242         } catch (final SAXException | IOException ex) {
243             throw new IllegalStateException(ex);
244         }
245         return errors;
246     }
247 
248     /**
249      * Creates a new validator.
250      * @param resolver The resolver for resources
251      * @return A new validator
252      */
253     private static Validator newValidator(final LSResourceResolver resolver) {
254         try {
255             final Validator validator = SchemaFactory
256                 .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
257                 .newSchema()
258                 .newValidator();
259             validator.setResourceResolver(resolver);
260             return validator;
261         } catch (final SAXException ex) {
262             throw new IllegalStateException(ex);
263         }
264     }
265 }