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 com.jcabi.log.Logger;
8   import java.util.ArrayList;
9   import java.util.Collection;
10  import java.util.Iterator;
11  import java.util.List;
12  import javax.xml.namespace.NamespaceContext;
13  import lombok.EqualsAndHashCode;
14  import org.cactoos.Scalar;
15  import org.cactoos.scalar.Sticky;
16  import org.cactoos.scalar.Unchecked;
17  import org.w3c.dom.Node;
18  import org.w3c.dom.ls.LSResourceResolver;
19  import org.xml.sax.SAXParseException;
20  
21  /**
22   * Strict {@link XML} that fails if encapsulated XML document
23   * doesn't validate against externally provided XSD schema or internally
24   * specified schema locations.
25   *
26   * <p>Objects of this class are immutable and thread-safe.
27   *
28   * @since 0.7
29   * @checkstyle AbbreviationAsWordInNameCheck (5 lines)
30   */
31  @SuppressWarnings("PMD.TooManyMethods")
32  @EqualsAndHashCode(of = "origin")
33  public final class StrictXML implements XML {
34  
35      /**
36       * Original XML document.
37       */
38      private final transient Unchecked<XML> origin;
39  
40      /**
41       * Public ctor.
42       * @param xml XML document
43       */
44      public StrictXML(final XML xml) {
45          this(xml, new ClasspathResolver());
46      }
47  
48      /**
49       * Public ctor.
50       * @param xml XML document
51       * @param resolver Custom resolver
52       * @since 0.19
53       */
54      public StrictXML(final XML xml, final LSResourceResolver resolver) {
55          this(xml, () -> xml.validate(resolver));
56      }
57  
58      /**
59       * Public ctor.
60       * @param xml XML document
61       * @param schema XSD schema
62       */
63      public StrictXML(final XML xml, final XML schema) {
64          this(xml, () -> xml.validate(schema));
65      }
66  
67      /**
68       * Private ctor.
69       * @param xml XML Document
70       * @param errs XML Document errors function
71       */
72      private StrictXML(
73          final XML xml,
74          final Scalar<Collection<SAXParseException>> errs
75      ) {
76          this(
77              () -> {
78                  final Collection<SAXParseException> errors = errs.value();
79                  if (!errors.isEmpty()) {
80                      Logger.warn(
81                          StrictXML.class,
82                          "%d XML validation error(s):\n  %s\n%s",
83                          errors.size(),
84                          StrictXML.join(StrictXML.print(errors), "\n  "),
85                          xml
86                      );
87                      throw new IllegalArgumentException(
88                          String.format(
89                              "%d error(s) in XML document: %s",
90                              errors.size(),
91                              StrictXML.join(StrictXML.print(errors), ";")
92                          )
93                      );
94                  }
95                  return xml;
96              }
97          );
98      }
99  
100     /**
101      * Default ctor.
102      * @param xml XML supplier
103      */
104     private StrictXML(final Scalar<XML> xml) {
105         this.origin = new Unchecked<>(new Sticky<>(xml));
106     }
107 
108     @Override
109     public String toString() {
110         return this.origin.value().toString();
111     }
112 
113     @Override
114     public List<String> xpath(final String query) {
115         return this.origin.value().xpath(query);
116     }
117 
118     @Override
119     public List<XML> nodes(final String query) {
120         return this.origin.value().nodes(query);
121     }
122 
123     @Override
124     public XML registerNs(final String prefix, final Object uri) {
125         return this.origin.value().registerNs(prefix, uri);
126     }
127 
128     @Override
129     public XML merge(final NamespaceContext context) {
130         return this.origin.value().merge(context);
131     }
132 
133     /**
134      * Retrieve DOM node, represented by this wrapper.
135      * This method works exactly the same as {@link #deepCopy()}.
136      * @return Deep copy of the inner DOM node.
137      * @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
138      * @checkstyle NoJavadocForOverriddenMethodsCheck (5 lines)
139      */
140     @Deprecated
141     @Override
142     public Node node() {
143         return this.origin.value().deepCopy();
144     }
145 
146     @Override
147     public Node inner() {
148         return this.origin.value().inner();
149     }
150 
151     @Override
152     public Node deepCopy() {
153         return this.origin.value().deepCopy();
154     }
155 
156     @Override
157     public Collection<SAXParseException> validate(final LSResourceResolver resolver) {
158         return this.origin.value().validate(resolver);
159     }
160 
161     @Override
162     public Collection<SAXParseException> validate(final XML xsd) {
163         return this.origin.value().validate(xsd);
164     }
165 
166     /**
167      * Convert errors to lines.
168      * @param errors The errors
169      * @return List of messages to print
170      */
171     private static Iterable<String> print(
172         final Collection<SAXParseException> errors
173     ) {
174         final Collection<String> lines = new ArrayList<>(errors.size());
175         for (final SAXParseException error : errors) {
176             lines.add(StrictXML.asMessage(error));
177         }
178         return lines;
179     }
180 
181     /**
182      * Turn violation into a message.
183      * @param violation The violation
184      * @return The message
185      */
186     private static String asMessage(final SAXParseException violation) {
187         final StringBuilder msg = new StringBuilder(100);
188         if (violation.getLineNumber() >= 0) {
189             msg.append('#').append(violation.getLineNumber());
190             if (violation.getColumnNumber() >= 0) {
191                 msg.append(':').append(violation.getColumnNumber());
192             }
193             msg.append(' ');
194         }
195         msg.append(violation.getLocalizedMessage());
196         if (violation.getException() != null) {
197             msg.append(" (")
198                 .append(violation.getException().getClass().getSimpleName())
199                 .append(')');
200         }
201         return msg.toString();
202     }
203 
204     /**
205      * Joins many objects' string representations with the given separator
206      * string. The separator will not be appended to the beginning or the end.
207      * @param iterable Iterable of objects.
208      * @param sep Separator string.
209      * @return Joined string.
210      */
211     private static String join(final Iterable<?> iterable, final String sep) {
212         final Iterator<?> iterator = iterable.iterator();
213         final Object first = iterator.next();
214         final StringBuilder buf = new StringBuilder(256);
215         if (first != null) {
216             buf.append(first);
217         }
218         while (iterator.hasNext()) {
219             buf.append(sep);
220             final Object obj = iterator.next();
221             if (obj != null) {
222                 buf.append(obj);
223             }
224         }
225         return buf.toString();
226     }
227 }