View Javadoc
1   /*
2    * Copyright (c) 2011-2025 Yegor Bugayenko
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.puppycrawl.tools.checkstyle.Checker;
34  import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
35  import com.puppycrawl.tools.checkstyle.PropertiesExpander;
36  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
37  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
38  import com.puppycrawl.tools.checkstyle.api.Configuration;
39  import com.qulice.spi.Environment;
40  import com.qulice.spi.ResourceValidator;
41  import com.qulice.spi.Violation;
42  import java.io.File;
43  import java.util.Collection;
44  import java.util.LinkedList;
45  import java.util.List;
46  import java.util.Properties;
47  import org.xml.sax.InputSource;
48  
49  /**
50   * Validator with Checkstyle.
51   *
52   * @since 0.3
53   * @checkstyle ClassDataAbstractionCoupling (260 lines)
54   */
55  public final class CheckstyleValidator implements ResourceValidator {
56  
57      /**
58       * Checkstyle checker.
59       */
60      private final Checker checker;
61  
62      /**
63       * Listener of checkstyle messages.
64        */
65      private final CheckstyleListener listener;
66  
67      /**
68       * Environment to use.
69       */
70      private final Environment env;
71  
72      /**
73       * Constructor.
74       * @param env Environment to use.
75       */
76      @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
77      public CheckstyleValidator(final Environment env) {
78          this.env = env;
79          this.checker = new Checker();
80          this.checker.setModuleClassLoader(
81              Thread.currentThread().getContextClassLoader()
82          );
83          try {
84              this.checker.configure(this.configuration());
85          } catch (final CheckstyleException ex) {
86              throw new IllegalStateException("Failed to configure checker", ex);
87          }
88          this.listener = new CheckstyleListener(this.env);
89          this.checker.addListener(this.listener);
90      }
91  
92      @Override
93      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
94      public Collection<Violation> validate(final Collection<File> files) {
95          final List<File> sources = this.getNonExcludedFiles(files);
96          try {
97              this.checker.process(sources);
98          } catch (final CheckstyleException ex) {
99              throw new IllegalStateException("Failed to process files", ex);
100         }
101         final List<AuditEvent> events = this.listener.events();
102         final Collection<Violation> results = new LinkedList<>();
103         for (final AuditEvent event : events) {
104             final String check = event.getSourceName();
105             results.add(
106                 new Violation.Default(
107                     this.name(),
108                     check.substring(check.lastIndexOf('.') + 1),
109                     event.getFileName(),
110                     String.valueOf(event.getLine()),
111                     event.getMessage()
112                 )
113             );
114         }
115         return results;
116     }
117 
118     @Override public String name() {
119         return "Checkstyle";
120     }
121 
122     /**
123      * Filters out excluded files from further validation.
124      * @param files Files to validate
125      * @return List of relevant files
126      */
127     public List<File> getNonExcludedFiles(final Collection<File> files) {
128         final List<File> relevant = new LinkedList<>();
129         for (final File file : files) {
130             final String name = file.getPath().substring(
131                 this.env.basedir().toString().length()
132             );
133             if (this.env.exclude("checkstyle", name)) {
134                 continue;
135             }
136             if (!name.matches("^.*\\.java$")) {
137                 continue;
138             }
139             relevant.add(file);
140         }
141         return relevant;
142     }
143 
144     /**
145      * Load checkstyle configuration.
146      * @return The configuration just loaded
147      * @see #validate(Collection)
148      */
149     private Configuration configuration() {
150         final File cache =
151             new File(this.env.tempdir(), "checkstyle/checkstyle.cache");
152         final File parent = cache.getParentFile();
153         if (!parent.exists() && !parent.mkdirs()) {
154             throw new IllegalStateException(
155                 String.format(
156                     "Unable to create directories needed for %s",
157                     cache.getPath()
158                 )
159             );
160         }
161         final Properties props = new Properties();
162         props.setProperty("cache.file", cache.getPath());
163         final InputSource src = new InputSource(
164             this.getClass().getResourceAsStream("checks.xml")
165         );
166         final Configuration config;
167         try {
168             config = ConfigurationLoader.loadConfiguration(
169                 src,
170                 new PropertiesExpander(props),
171                 ConfigurationLoader.IgnoredModulesOptions.OMIT
172             );
173         } catch (final CheckstyleException ex) {
174             throw new IllegalStateException("Failed to load config", ex);
175         }
176         return config;
177     }
178 }