View Javadoc
1   /*
2    * Copyright (c) 2011-2024 Qulice.com
3    *
4    * All rights reserved.
5    *
6    * Redistribution and use in source and binary forms, with or without
7    * modification, are permitted provided that the following conditions
8    * are met: 1) Redistributions of source code must retain the above
9    * copyright notice, this list of conditions and the following
10   * disclaimer. 2) Redistributions in binary form must reproduce the above
11   * copyright notice, this list of conditions and the following
12   * disclaimer in the documentation and/or other materials provided
13   * with the distribution. 3) Neither the name of the Qulice.com nor
14   * the names of its contributors may be used to endorse or promote
15   * products derived from this software without specific prior written
16   * permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
20   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29   * OF THE POSSIBILITY OF SUCH DAMAGE.
30   */
31  package com.qulice.checkstyle;
32  
33  import com.jcabi.log.Logger;
34  import com.puppycrawl.tools.checkstyle.Checker;
35  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
36  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
37  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
38  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
39  import com.puppycrawl.tools.checkstyle.api.Configuration;
40  import com.qulice.spi.Environment;
41  import com.qulice.spi.ResourceValidator;
42  import com.qulice.spi.Violation;
43  import java.io.File;
44  import java.io.IOException;
45  import java.net.MalformedURLException;
46  import java.net.URL;
47  import java.nio.file.Paths;
48  import java.util.Collection;
49  import java.util.LinkedList;
50  import java.util.List;
51  import java.util.Properties;
52  import org.cactoos.text.IoCheckedText;
53  import org.cactoos.text.Replaced;
54  import org.cactoos.text.TextOf;
55  import org.cactoos.text.Trimmed;
56  import org.xml.sax.InputSource;
57  
58  /**
59   * Validator with Checkstyle.
60   *
61   * @since 0.3
62   * @checkstyle ClassDataAbstractionCoupling (260 lines)
63   */
64  public final class CheckstyleValidator implements ResourceValidator {
65  
66      /**
67       * Checkstyle checker.
68       */
69      private final Checker checker;
70  
71      /**
72       * Listener of checkstyle messages.
73        */
74      private final CheckstyleListener listener;
75  
76      /**
77       * Environment to use.
78       */
79      private final Environment env;
80  
81      /**
82       * Constructor.
83       * @param env Environment to use.
84       */
85      @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
86      public CheckstyleValidator(final Environment env) {
87          this.env = env;
88          this.checker = new Checker();
89          this.checker.setModuleClassLoader(
90              Thread.currentThread().getContextClassLoader()
91          );
92          try {
93              this.checker.configure(this.configuration());
94          } catch (final CheckstyleException ex) {
95              throw new IllegalStateException("Failed to configure checker", ex);
96          }
97          this.listener = new CheckstyleListener(this.env);
98          this.checker.addListener(this.listener);
99      }
100 
101     @Override
102     @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
103     public Collection<Violation> validate(final Collection<File> files) {
104         final List<File> sources = this.getNonExcludedFiles(files);
105         try {
106             this.checker.process(sources);
107         } catch (final CheckstyleException ex) {
108             throw new IllegalStateException("Failed to process files", ex);
109         }
110         final List<AuditEvent> events = this.listener.events();
111         final Collection<Violation> results = new LinkedList<>();
112         for (final AuditEvent event : events) {
113             final String check = event.getSourceName();
114             results.add(
115                 new Violation.Default(
116                     this.name(),
117                     check.substring(check.lastIndexOf('.') + 1),
118                     event.getFileName(),
119                     String.valueOf(event.getLine()),
120                     event.getMessage()
121                 )
122             );
123         }
124         return results;
125     }
126 
127     @Override public String name() {
128         return "Checkstyle";
129     }
130 
131     /**
132      * Filters out excluded files from further validation.
133      * @param files Files to validate
134      * @return List of relevant files
135      */
136     public List<File> getNonExcludedFiles(final Collection<File> files) {
137         final List<File> relevant = new LinkedList<>();
138         for (final File file : files) {
139             final String name = file.getPath().substring(
140                 this.env.basedir().toString().length()
141             );
142             if (this.env.exclude("checkstyle", name)) {
143                 continue;
144             }
145             if (!name.matches("^.*\\.java$")) {
146                 continue;
147             }
148             relevant.add(file);
149         }
150         return relevant;
151     }
152 
153     /**
154      * Load checkstyle configuration.
155      * @return The configuration just loaded
156      * @see #validate(Collection)
157      */
158     private Configuration configuration() {
159         final File cache =
160             new File(this.env.tempdir(), "checkstyle/checkstyle.cache");
161         final File parent = cache.getParentFile();
162         if (!parent.exists() && !parent.mkdirs()) {
163             throw new IllegalStateException(
164                 String.format(
165                     "Unable to create directories needed for %s",
166                     cache.getPath()
167                 )
168             );
169         }
170         final Properties props = new Properties();
171         props.setProperty("cache.file", cache.getPath());
172         props.setProperty("header", this.header());
173         final InputSource src = new InputSource(
174             this.getClass().getResourceAsStream("checks.xml")
175         );
176         final Configuration config;
177         try {
178             config = ConfigurationLoader.loadConfiguration(
179                 src,
180                 new PropertiesExpander(props),
181                 ConfigurationLoader.IgnoredModulesOptions.OMIT
182             );
183         } catch (final CheckstyleException ex) {
184             throw new IllegalStateException("Failed to load config", ex);
185         }
186         return config;
187     }
188 
189     /**
190      * Create header content, from file.
191      * @return The content of header
192      * @see #configuration()
193      */
194     @SuppressWarnings("PMD.InefficientEmptyStringCheck")
195     private String header() {
196         final String name = this.env.param("license", "LICENSE.txt");
197         final URL url = this.toUrl(name);
198         final String content;
199         try {
200             content = new IoCheckedText(
201                 new Replaced(
202                     new Trimmed(
203                         new TextOf(
204                             url.openStream()
205                         )
206                     ),
207                     "[\\r\\n]+$",
208                     ""
209                 )
210             ).asString();
211         } catch (final IOException ex) {
212             throw new IllegalStateException("Failed to read license", ex);
213         }
214         final StringBuilder builder = new StringBuilder(100);
215         final String eol = System.lineSeparator();
216         builder.append("/*").append(eol);
217         for (final String line : CheckstyleValidator.splitPreserve(content, eol)) {
218             builder.append(" *");
219             if (!line.trim().isEmpty()) {
220                 builder.append(' ').append(line.trim());
221             }
222             builder.append(eol);
223         }
224         builder.append(" */");
225         final String license = builder.toString();
226         Logger.debug(this, "LICENSE found: %s", url);
227         Logger.debug(
228             this,
229             "LICENSE full text after parsing:\n%s",
230             license
231         );
232         return license;
233     }
234 
235     /**
236      * Convert file name to URL.
237      * @param name The name of file
238      * @return The URL
239      * @see #header()
240      */
241     private URL toUrl(final String name) {
242         final URL url;
243         if (name.startsWith("file:")) {
244             try {
245                 url = Paths.get(name.substring(5)).toUri().toURL();
246             } catch (final MalformedURLException ex) {
247                 throw new IllegalStateException("Invalid URL", ex);
248             }
249         } else {
250             url = this.env.classloader().getResource(name);
251             if (url == null) {
252                 throw new IllegalStateException(
253                     String.format(
254                         "'%s' resource is not found in classpath",
255                         name
256                     )
257                 );
258             }
259         }
260         return url;
261     }
262 
263     /**
264      * Divide string using separators to the parts adding empty lines for
265      * two consistently separators.
266      * @param content String line
267      * @param separators Separators string
268      * @return List of line parts
269      */
270     private static List<String> splitPreserve(final String content, final String separators) {
271         final List<String> tokens = new LinkedList<>();
272         final int len = content.length();
273         int ind = 0;
274         int start = 0;
275         while (ind < len) {
276             if (separators.indexOf(content.charAt(ind)) >= 0) {
277                 tokens.add(content.substring(start, ind));
278                 start = ind + 1;
279             }
280             ++ind;
281         }
282         tokens.add(content.substring(start, ind));
283         return tokens;
284     }
285 }