View Javadoc
1   /*
2    * Copyright (c) 2011-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.w3c;
31  
32  import com.jcabi.aspects.Immutable;
33  import com.jcabi.http.Request;
34  import com.jcabi.http.Response;
35  import com.jcabi.http.request.JdkRequest;
36  import com.jcabi.http.response.XmlResponse;
37  import com.jcabi.log.Logger;
38  import com.jcabi.xml.XML;
39  import java.io.IOException;
40  import java.net.HttpURLConnection;
41  import java.net.URI;
42  import java.util.Arrays;
43  import java.util.List;
44  import java.util.regex.Pattern;
45  import javax.ws.rs.core.HttpHeaders;
46  import javax.ws.rs.core.MediaType;
47  import javax.ws.rs.core.UriBuilder;
48  import lombok.EqualsAndHashCode;
49  import lombok.ToString;
50  
51  /**
52   * Default implementation of CSS validator.
53   *
54   * @see <a href="http://jigsaw.w3.org/css-validator/api.html">W3C API</a>
55   * @since 0.1
56   */
57  @Immutable
58  @ToString
59  @EqualsAndHashCode(callSuper = false, of = "uri")
60  final class DefaultCssValidator
61      extends AbstractBaseValidator implements Validator {
62  
63      /**
64       * The URI to use in W3C.
65       */
66      private final transient String uri;
67  
68      /**
69       * Public ctor.
70       * @param entry Entry point to use
71       */
72      DefaultCssValidator(final URI entry) {
73          super();
74          this.uri = entry.toString();
75      }
76  
77      @Override
78      public ValidationResponse validate(final String css)
79          throws IOException {
80          final ValidationResponse response;
81          final Pattern pattern = Pattern.compile(
82              ".*^/\\* JIGSAW IGNORE: [^\\n]+\\*/$.*",
83              Pattern.MULTILINE | Pattern.DOTALL
84          );
85          try {
86              if (pattern.matcher(css).matches()) {
87                  response = AbstractBaseValidator.success("");
88              } else {
89                  response = this.processed(css);
90              }
91          } catch (final IllegalArgumentException ex) {
92              throw new IOException(ex);
93          }
94          return response;
95      }
96  
97      /**
98       * Return a response after real processing of the CSS.
99       * @param css The CSS stylesheet to check
100      * @return The response
101      * @throws IOException if fails
102      */
103     private ValidationResponse processed(final String css) throws IOException {
104         final Request req = this.request(
105             AbstractBaseValidator.entity(
106                 "file", DefaultCssValidator.filter(css), "text/css"
107             )
108         );
109         final Response response = DefaultCssValidator.correct(req.fetch());
110         return DefaultCssValidator.build(
111             response.as(XmlResponse.class)
112                 .registerNs("env", "http://www.w3.org/2003/05/soap-envelope")
113                 .registerNs("m", "http://www.w3.org/2005/07/css-validator")
114                 .assertXPath("//m:validity")
115                 .assertXPath("//m:checkedby")
116                 .xml()
117         );
118     }
119 
120     /**
121      * Send request and return response.
122      * @param entity The entity to POST
123      * @return The response
124      */
125     private Request request(final String entity) {
126         return new JdkRequest(this.uri)
127             .method(Request.POST)
128             .body().set(entity).back()
129             .header(HttpHeaders.USER_AGENT, AbstractBaseValidator.USER_AGENT)
130             .header(HttpHeaders.ACCEPT, "application/soap+xml")
131             .header(
132                 HttpHeaders.CONTENT_TYPE,
133                 Logger.format(
134                     "%s; boundary=%s",
135                     MediaType.MULTIPART_FORM_DATA,
136                     AbstractBaseValidator.BOUNDARY
137                 )
138             );
139     }
140 
141     /**
142      * Build response from XML.
143      * @param soap The response
144      * @return The validation response just built
145      */
146     private static ValidationResponse build(final XML soap) {
147         final DefaultValidationResponse resp = new DefaultValidationResponse(
148             "true".equals(
149                 AbstractBaseValidator.textOf(soap.xpath("//m:validity/text()"))
150             ),
151             UriBuilder.fromUri(
152                 AbstractBaseValidator.textOf(soap.xpath("//m:checkedby/text()"))
153             ).build(),
154             AbstractBaseValidator.textOf(soap.xpath("//m:doctype/text()")),
155             AbstractBaseValidator.charset(
156                 AbstractBaseValidator.textOf(soap.xpath("//m:charset/text()"))
157             )
158         );
159         for (final XML node : soap.nodes("//m:error")) {
160             resp.addError(DefaultCssValidator.defect(node));
161         }
162         for (final XML node : soap.nodes("//m:warning")) {
163             resp.addWarning(DefaultCssValidator.defect(node));
164         }
165         return resp;
166     }
167 
168     /**
169      * Convert SOAP node to defect.
170      * @param node The node
171      * @return The defect
172      */
173     private static Defect defect(final XML node) {
174         return new Defect(
175             AbstractBaseValidator.intOf(node.xpath("m:line/text()")),
176             AbstractBaseValidator.intOf(node.xpath("m:col/text()")),
177             AbstractBaseValidator.textOf(node.xpath("m:source/text()")),
178             AbstractBaseValidator.textOf(node.xpath("m:explanation/text()")),
179             AbstractBaseValidator.textOf(node.xpath("m:messageid/text()")),
180             AbstractBaseValidator.textOf(node.xpath("m:message/text()"))
181         );
182     }
183 
184     /**
185      * Check if response from W3C contains some bad status.
186      * @param response Response from W3c.
187      * @return Response passed as parameter.
188      * @throws IOException when has some bad status.
189      */
190     private static Response correct(final Response response)
191         throws IOException {
192         final List<Integer> statuses = Arrays.asList(
193             HttpURLConnection.HTTP_INTERNAL_ERROR,
194             HttpURLConnection.HTTP_NOT_IMPLEMENTED,
195             HttpURLConnection.HTTP_BAD_GATEWAY,
196             HttpURLConnection.HTTP_UNAVAILABLE,
197             HttpURLConnection.HTTP_GATEWAY_TIMEOUT,
198             HttpURLConnection.HTTP_VERSION
199         );
200         if (statuses.contains(response.status())) {
201             throw new IOException(
202                 String.format(
203                     "Bad status from W3C server: %1d",
204                     response.status()
205                 )
206             );
207         }
208         return response;
209     }
210 
211     /**
212      * Exclude problematic lines from CSS.
213      * @param css The css document
214      * @return New document, with lines excluded
215      */
216     private static String filter(final String css) {
217         return Pattern.compile(
218             "^/\\* JIGSAW: [^\\n]+\\*/$",
219             Pattern.MULTILINE | Pattern.DOTALL
220         ).matcher(css).replaceAll("");
221     }
222 
223 }