View Javadoc
1   package com.github.mikkoi.maven.plugins.enforcer.rule.charsetencoding;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import com.google.common.io.ByteStreams;
23  import org.apache.maven.enforcer.rule.api.EnforcerRule;
24  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
25  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
26  import org.apache.maven.plugin.logging.Log;
27  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
28  
29  import javax.annotation.Nonnull;
30  import javax.annotation.Nullable;
31  import java.io.File;
32  import java.io.FileInputStream;
33  import java.io.IOException;
34  import java.nio.ByteBuffer;
35  import java.nio.charset.CharacterCodingException;
36  import java.nio.charset.Charset;
37  import java.nio.charset.CharsetDecoder;
38  import java.nio.charset.IllegalCharsetNameException;
39  import java.nio.charset.UnsupportedCharsetException;
40  import java.nio.file.FileVisitOption;
41  import java.nio.file.FileVisitResult;
42  import java.nio.file.FileVisitor;
43  import java.nio.file.Files;
44  import java.nio.file.Path;
45  import java.nio.file.Paths;
46  import java.nio.file.SimpleFileVisitor;
47  import java.nio.file.attribute.BasicFileAttributes;
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.LinkedHashSet;
51  import java.util.Set;
52  import java.util.regex.Pattern;
53  
54  /**
55   * Checks file encodings to see if they match the parameter
56   * or Maven property project.build.sourceEncoding.
57   * If file requireEncoding can not be determined it is skipped.
58   */
59  @SuppressWarnings("WeakerAccess")
60  public final class CharacterSetEncodingRule implements EnforcerRule {
61  
62      @Nonnull
63      private static final String INCLUDE_REGEX_DEFAULT = ".*";
64      @Nonnull
65      private static final String EXCLUDE_REGEX_DEFAULT = "";
66      @Nonnull
67      private static final String DIRECTORY_DEFAULT = "src";
68  
69      /**
70       * Faulty files list. Can be accessed after processing execute().
71       */
72      @Nonnull
73      private final Collection<FileResult> faultyFiles = new ArrayList<>();
74      /**
75       * Validate files must match this requireEncoding.
76       * Default: ${project.builder.sourceEncoding}.
77       */
78      @Nullable
79      private String requireEncoding = null;
80      /**
81       * Directory to search for files.
82       */
83      @Nullable
84      private String directory = null;
85      /**
86       * Regular Expression to match file names against for filtering in.
87       */
88      @Nullable
89      private String includeRegex = null;
90      /**
91       * Regular Expression to match file names against for filtering out
92       * Can be used together with includeRegex.
93       * includeRegex will first pick files in,
94       * then excludeRegex will filter out files from the selected ones.
95       */
96      @Nullable
97      private String excludeRegex = null;
98  
99      /**
100      * Get the faulty files list.
101      */
102     @Nonnull
103     public Collection<FileResult> getFaultyFiles() {
104         return faultyFiles;
105     }
106 
107     /**
108      * @param helper EnforcerRuleHelper
109      * @throws EnforcerRuleException Throws when error
110      */
111     public void execute(@Nonnull final EnforcerRuleHelper helper)
112             throws EnforcerRuleException {
113         Log log = helper.getLog();
114 
115         try {
116             // get the various expressions out of the helper.
117             String basedir = helper.evaluate("${project.basedir}").toString();
118 
119             log.debug("Retrieved Basedir: " + basedir);
120             log.debug("requireEncoding: " + (requireEncoding == null ? "null" : requireEncoding));
121             log.debug("directory: " + (directory == null ? "null" : directory));
122             log.debug("includeRegex: " + (includeRegex == null ? "null" : includeRegex));
123             log.debug("excludeRegex: " + (excludeRegex == null ? "null" : excludeRegex));
124 
125             if (this.getRequireEncoding() == null || this.getRequireEncoding().trim().length() == 0) {
126                 final String sourceEncoding = (String) helper.evaluate("${project.build.sourceEncoding}");
127                 log.info("No parameter 'requiredEncoding' set. Defaults to property 'project.build.sourceEncoding'.");
128                 if (sourceEncoding != null && sourceEncoding.trim().length() > 0) {
129                     this.setRequireEncoding(sourceEncoding);
130                 } else {
131                     throw new EnforcerRuleException("Missing parameter 'requireEncoding' and property 'project.build.sourceEncoding'.");
132                 }
133             }
134             try {
135                 Charset.forName(this.getRequireEncoding()); //  Charset names are not case-sensitive
136             } catch (IllegalCharsetNameException e) {
137                 throw new EnforcerRuleException("Illegal value (illegal character set name) '" + requireEncoding + "' for parameter 'requireEncoding'.");
138             } catch (UnsupportedCharsetException e) {
139                 throw new EnforcerRuleException("Illegal value (not supported character set) '" + requireEncoding + "' for parameter 'requireEncoding'.");
140             } catch (IllegalArgumentException e) {
141                 throw new EnforcerRuleException("Illegal value (empty) '" + requireEncoding + "' for parameter 'requireEncoding'.");
142             }
143             if (this.getDirectory() == null || this.getDirectory().trim().length() == 0) {
144                 log.info("No parameter 'directory' set. Defaults to '" + DIRECTORY_DEFAULT + "'.");
145                 this.setDirectory(DIRECTORY_DEFAULT);
146             }
147             if (this.getIncludeRegex() == null || this.getIncludeRegex().trim().length() == 0) {
148                 log.info("No parameter 'includeRegex' set. Defaults to '" + INCLUDE_REGEX_DEFAULT + "'.");
149                 this.setIncludeRegex(INCLUDE_REGEX_DEFAULT);
150             }
151             if (this.getExcludeRegex() == null || this.getExcludeRegex().trim().length() == 0) {
152                 log.info("No parameter 'excludeRegex' set. Defaults to '" + EXCLUDE_REGEX_DEFAULT + "'.");
153                 this.setExcludeRegex(EXCLUDE_REGEX_DEFAULT);
154             }
155             log.debug("requireEncoding: " + this.getRequireEncoding());
156             log.debug("directory: " + this.getDirectory());
157             log.debug("includeRegex: " + this.getIncludeRegex());
158             log.debug("excludeRegex: " + this.getExcludeRegex());
159 
160             // Check the existence of the wanted directory:
161             final Path dir = Paths.get(basedir, getDirectory());
162             log.debug("Get files in dir '" + dir.toString() + "'.");
163             if (!dir.toFile().exists()) {
164                 throw new EnforcerRuleException(
165                         "Directory '" + dir.toString() + "' not found."
166                                 + " Specified by parameter 'directory' (value: '" + this.getDirectory() + "')!"
167                 );
168             }
169 
170             // Put all files into this collection:
171             Collection<FileResult> allFiles = getFileResults(log, dir);
172 
173             // Copy faulty files to another list.
174             log.debug("Moving possible faulty files (faulty encoding) to another list.");
175             for (FileResult res : allFiles) {
176                 log.debug("Checking if file '" + res.getPath().toString() + "' has encoding '" + requireEncoding + "'.");
177                 boolean hasCorrectEncoding = true;
178                 try (FileInputStream fileInputStream = new FileInputStream(res.getPath().toFile())) {
179                     byte[] bytes = ByteStreams.toByteArray(fileInputStream);
180                     Charset charset = Charset.forName(this.getRequireEncoding());
181                     CharsetDecoder decoder = charset.newDecoder();
182                     ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
183                     decoder.decode(byteBuffer);
184                 } catch (CharacterCodingException e) {
185                     hasCorrectEncoding = false;
186                 } catch (IOException e) {
187                     e.printStackTrace();
188                     log.error(e.getMessage());
189                     hasCorrectEncoding = false;
190                 }
191                 if (!hasCorrectEncoding) {
192                     log.debug("Moving faulty file: " + res.getPath());
193                     FileResult faultyFile = new FileResult.Builder(res.getPath())
194                             .lastModified(res.getLastModified())
195                             .build();
196                     faultyFiles.add(faultyFile);
197                 } else {
198                     log.debug("Has correct encoding. Not moving to faulty files list.");
199                 }
200             }
201             log.debug("All faulty files moved.");
202 
203             // Report
204             if (!faultyFiles.isEmpty()) {
205                 final StringBuilder builder = new StringBuilder();
206                 builder.append("Wrong encoding in following files:");
207                 builder.append(System.getProperty("line.separator"));
208                 for (FileResult res : faultyFiles) {
209                     builder.append(res.getPath());
210                     builder.append(System.getProperty("line.separator"));
211                 }
212                 throw new EnforcerRuleException(builder.toString());
213             }
214         } catch (ExpressionEvaluationException e) {
215             throw new EnforcerRuleException(
216                     "Unable to lookup an expression " + e.getLocalizedMessage(), e
217             );
218         }
219     }
220 
221     @Nonnull
222     private Collection<FileResult> getFileResults(final Log log, final Path dir) {
223         Collection<FileResult> allFiles = new ArrayList<>();
224         FileVisitor<Path> fileVisitor = new GetEncodingsFileVisitor(
225                 log,
226                 this.getIncludeRegex() != null ? this.getIncludeRegex() : INCLUDE_REGEX_DEFAULT,
227                 this.getExcludeRegex() != null ? this.getExcludeRegex() : EXCLUDE_REGEX_DEFAULT,
228                 allFiles
229         );
230         try {
231             Set<FileVisitOption> visitOptions = new LinkedHashSet<>();
232             visitOptions.add(FileVisitOption.FOLLOW_LINKS);
233             Files.walkFileTree(dir,
234                     visitOptions,
235                     Integer.MAX_VALUE,
236                     fileVisitor
237             );
238         } catch (Exception e) {
239             log.error(e.getCause() + e.getMessage());
240         }
241         return allFiles;
242     }
243 
244     /**
245      * If your rule is cacheable, you must return a unique id when parameters or conditions
246      * change that would cause the result to be different. Multiple cached results are stored
247      * based on their id.
248      * <p>
249      * The easiest way to do this is to return a hash computed from the values of your parameters.
250      * <p>
251      * If your rule is not cacheable, then the result here is not important, you may return anything.
252      *
253      * @return Always false here.
254      */
255     @Nullable
256     public String getCacheId() {
257         return String.valueOf(false);
258     }
259 
260     /**
261      * This tells the system if the results are cacheable at all. Keep in mind that during
262      * forked builds and other things, a given rule may be executed more than once for the same
263      * project. This means that even things that change from project to project may still
264      * be cacheable in certain instances.
265      *
266      * @return Always false here.
267      */
268     public boolean isCacheable() {
269         return false;
270     }
271 
272     /**
273      * If the rule is cacheable and the same id is found in the cache, the stored results
274      * are passed to this method to allow double checking of the results. Most of the time
275      * this can be done by generating unique ids, but sometimes the results of objects returned
276      * by the helper need to be queried. You may for example, store certain objects in your rule
277      * and then query them later.
278      *
279      * @param arg0 EnforcerRule
280      * @return Always false here.
281      */
282     public boolean isResultValid(@Nullable final EnforcerRule arg0) {
283         return false;
284     }
285 
286     /**
287      * Getters and setters for the parameters (these are filled by Maven).
288      */
289 
290     @SuppressWarnings("WeakerAccess")
291     @Nullable
292     public String getDirectory() {
293         return directory;
294     }
295 
296     @SuppressWarnings("WeakerAccess")
297     public void setDirectory(@Nullable final String directory) {
298         this.directory = directory;
299     }
300 
301     @SuppressWarnings("WeakerAccess")
302     @Nullable
303     public String getIncludeRegex() {
304         return includeRegex;
305     }
306 
307     @SuppressWarnings("WeakerAccess")
308     public void setIncludeRegex(@Nullable final String includeRegex) {
309         this.includeRegex = includeRegex;
310     }
311 
312     @SuppressWarnings("WeakerAccess")
313     @Nullable
314     public String getExcludeRegex() {
315         return excludeRegex;
316     }
317 
318     @SuppressWarnings("WeakerAccess")
319     public void setExcludeRegex(@Nullable final String excludeRegex) {
320         this.excludeRegex = excludeRegex;
321     }
322 
323     @SuppressWarnings("WeakerAccess")
324     @Nullable
325     public String getRequireEncoding() {
326         return requireEncoding;
327     }
328 
329     @SuppressWarnings("WeakerAccess")
330     public void setRequireEncoding(@Nullable final String requireEncoding) {
331         this.requireEncoding = requireEncoding;
332     }
333 
334     /**
335      * Extended SimpleFileVisitor for walking through the files.
336      */
337     private static class GetEncodingsFileVisitor extends SimpleFileVisitor<Path> {
338         @Nonnull
339         private final Log log;
340         private final boolean includeRegexUsed;
341         @Nonnull
342         private final Pattern includeRegexPattern;
343         private final boolean excludeRegexUsed;
344         @Nonnull
345         private final Pattern excludeRegexPattern;
346         @Nonnull
347         private final Collection<FileResult> results;
348 
349         /**
350          * Constructor.
351          *
352          * @param pluginLog    Maven Plugin logging channel.
353          * @param includeRegex Include regex pattern.
354          * @param excludeRegex Exclude regex pattern.
355          * @param fileResults  Initialized collection to be filled.
356          */
357         GetEncodingsFileVisitor(
358                 @Nonnull final Log pluginLog,
359                 @Nonnull final String includeRegex,
360                 @Nonnull final String excludeRegex,
361                 @Nonnull final Collection<FileResult> fileResults
362         ) {
363             this.log = pluginLog;
364             // Attn. Because we have includeRegex default (.*) which replaces
365             // an empty includeRegex, includeRegex can never have length 0 chars!
366             // But excludeRegex can have length 0 chars!
367             includeRegexUsed = true;
368             includeRegexPattern = Pattern.compile(includeRegex);
369             if (excludeRegex.length() > 0) {
370                 excludeRegexUsed = true;
371                 excludeRegexPattern = Pattern.compile(excludeRegex);
372             } else {
373                 excludeRegexUsed = false;
374                 excludeRegexPattern = Pattern.compile("");
375             }
376             this.results = fileResults;
377         }
378 
379         @Override
380         public FileVisitResult visitFile(
381                 final Path aFile, final BasicFileAttributes aAttrs
382         ) throws IOException {
383             log.debug("Visiting file '" + aFile.toString() + "'.");
384             if (includeRegexUsed && !includeRegexPattern.matcher(aFile.toString()).find()) {
385                 log.debug("File not matches includeRegex in-filter. Exclude file from list!");
386                 return FileVisitResult.CONTINUE;
387             }
388             if (excludeRegexUsed && excludeRegexPattern.matcher(aFile.toString()).find()) {
389                 log.debug("File matches excludeRegex out-filter. Exclude file from list!");
390                 return FileVisitResult.CONTINUE;
391             }
392             log.debug("File matches includeRegex in-filter and not matches excludeRegex out-filter. Include file to list!");
393             File file = aFile.toFile();
394             FileResult res = new FileResult.Builder(aFile.toAbsolutePath())
395                     .lastModified(file.lastModified())
396                     .build();
397             results.add(res);
398             return FileVisitResult.CONTINUE;
399         }
400 
401         @Override
402         public FileVisitResult preVisitDirectory(
403                 final Path aDir, final BasicFileAttributes aAttrs
404         ) throws IOException {
405             log.debug("Visiting directory '" + aDir.toString() + "'.");
406             return FileVisitResult.CONTINUE;
407         }
408 
409     }
410 
411 }