View Javadoc
1   package com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage;
2   
3   import com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage.UsageFiles.UsageLocation;
4   import com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage.configuration.Definitions;
5   import com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage.configuration.FileSpecs;
6   import com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage.configuration.Templates;
7   import com.github.mikkoi.maven.plugins.enforcer.rule.propertyusage.configuration.Usages;
8   import org.apache.maven.enforcer.rule.api.EnforcerRule;
9   import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
10  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
11  import org.apache.maven.plugin.logging.Log;
12  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
13  import org.codehaus.plexus.util.StringUtils;
14  
15  import javax.annotation.Nonnull;
16  import javax.annotation.Nullable;
17  import java.io.IOException;
18  import java.nio.charset.Charset;
19  import java.nio.file.Path;
20  import java.nio.file.Paths;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.concurrent.ConcurrentHashMap;
26  import java.util.stream.Collectors;
27  
28  /**
29   * Verifies for usage of properties mentioned in .properties files.
30   */
31  public final class PropertyUsageRule implements EnforcerRule {
32  
33      /**
34       * Default character set for all files to read.
35       */
36      private static final Charset DEFAULT_CHAR_SET = Charset.forName("UTF-8");
37  
38      /**
39       * Properties which were defined more than once.
40       */
41      @Nonnull
42      private final Map<String, Integer> propertiesDefinedMoreThanOnce = new ConcurrentHashMap<>();
43  
44      /**
45       * Properties which were not found in usages.
46       */
47      @Nonnull
48      private final Set<String> propertiesNotUsed = new HashSet<>();
49  
50      /**
51       * Properties which were used in usages but not defined in definitions.
52       */
53      @Nonnull
54      private final Set<UsageFiles.UsageLocation> propertiesNotDefined = new HashSet<>();
55  
56      /**
57       * All properties defined.
58       */
59      @Nonnull
60      private final Map<String, Set<PropertyDefinition>> propertiesDefined = new ConcurrentHashMap<>();
61  
62      /**
63       * Logger given by Maven Enforcer.
64       */
65      Log log;
66  
67      //
68      // Following variables match the configuration items
69      // and are populated by Maven/Enforcer, despite being private. (!)
70      // I'm not sure I want to known how it happens.
71      //
72  
73      /**
74       * Character encoding for source (usage) files.
75       */
76      private String sourceEncoding = DEFAULT_CHAR_SET.toString();//NOPMD
77  
78      /**
79       * Character encoding for properties files.
80       */
81      private String propertiesEncoding = DEFAULT_CHAR_SET.toString();//NOPMD
82  
83      /**
84       * Activate definitionsOnlyOnce
85       */
86      private boolean definitionsOnlyOnce = true;
87  
88      /**
89       * Activate definedPropertiesAreUsed
90       */
91      private boolean definedPropertiesAreUsed = true;
92  
93      /**
94       * Activate usedPropertiesAreDefined
95       */
96      private boolean usedPropertiesAreDefined = false;
97  
98      /**
99       * Activate reportDuplicateDefinitions
100      */
101     private boolean reportDuplicateDefinitions = false;
102 
103     /**
104      * Replace this string with property name in template(s).
105      */
106     @Nonnull
107     private String replaceInTemplateWithPropertyName = Templates.DEFAULT_REPLACE_IN_TEMPLATE_WITH_PROPERTY_NAME;
108 
109     /**
110      * Replace template property name placeholder with this
111      * when searching for properties.
112      */
113     @Nonnull
114     private String propertyNameRegexp = Templates.PROPERTY_NAME_REGEXP;//NOPMD
115 
116     /**
117      * Definitions
118      */
119     @Nonnull
120     private Collection<String> definitions = Definitions.getDefault();
121     /**
122      * Templates
123      */
124     @Nonnull
125     private Collection<String> templates = Templates.getDefault();
126     /**
127      * Usages
128      */
129     @Nonnull
130     private Collection<String> usages = Usages.getDefault();
131 
132     /**
133      * @param helper EnforcerRuleHelper
134      * @throws EnforcerRuleException Throws when error
135      */
136     @Override
137     @SuppressWarnings({
138             "squid:S3776",  // Cognitive Complexity of methods should not be too high
139             "squid:S1067",  // Expressions should not be too complex
140             "squid:S1192",  // String literals should not be duplicated
141             "squid:MethodCyclomaticComplexity"
142     })
143     //@SuppressWarnings("squid:S1192")
144     public void execute(@Nonnull final EnforcerRuleHelper helper)
145             throws EnforcerRuleException {
146         log = helper.getLog();
147 
148         Path basedir = Paths.get("");
149         try {
150             basedir = Paths.get(helper.evaluate("${project.basedir}").toString());
151         } catch (ExpressionEvaluationException e) {
152             log.error("Cannot get property 'project.basedir'. Using current working directory. Error:" + e);
153         }
154 
155         Charset propertiesEnc = DEFAULT_CHAR_SET;
156         Charset sourceEnc = DEFAULT_CHAR_SET;
157         try {
158             if (StringUtils.isNotBlank(propertiesEncoding)) {
159                 propertiesEnc = Charset.forName(propertiesEncoding);
160             } else {
161                 propertiesEnc = Charset.forName(helper.evaluate("${project.build.sourceEncoding}").toString());
162             }
163             if (StringUtils.isNotBlank(sourceEncoding)) {
164                 sourceEnc = Charset.forName(sourceEncoding);
165             } else {
166                 sourceEnc = Charset.forName(helper.evaluate("${project.build.sourceEncoding}").toString());
167             }
168         } catch (ExpressionEvaluationException e) {
169             log.error("Cannot get property 'project.build.sourceEncoding'. Using default (UTF-8). Error:" + e);
170         }
171 
172         log.debug("PropertyUsageRule:execute() - Settings:");
173         log.debug("basedir:" + basedir);
174         log.debug("propertiesEnc:" + propertiesEnc);
175         log.debug("sourceEnc:" + sourceEnc);
176         log.debug("replaceInTemplateWithPropertyName:" + replaceInTemplateWithPropertyName);
177         log.debug("propertyNameRegexp:" + propertyNameRegexp);
178         log.debug("definitions:" + definitions);
179         log.debug("templates:" + templates);
180         log.debug("usages:" + usages);
181 
182         try {
183             log.debug("PropertyUsageRule:execute() - Run:");
184             // Get property definitions (i.e. property names):
185             // Get all the fileSpecs to read for the properties definitions.
186             log.debug("definitions:");
187             definitions.stream().forEach(a -> log.debug(a));
188             log.debug(":END");
189             final Collection<String> propertyFilenames = FileSpecs.getAbsoluteFilenames(definitions, basedir, log)
190                     .stream().sorted().collect(Collectors.toSet());
191                     // Get the property definitions and how many times they are defined.
192             Map<String, Set<PropertyDefinition>> definedProperties = getPropertiesDefined(propertiesEnc, propertyFilenames);
193             definedProperties.forEach((prop, defs) -> {
194                 log.debug("Property '" + prop + "' defined " + defs.size() + " times.");
195                 if (defs.size() > 1) {
196                     propertiesDefinedMoreThanOnce.put(prop, defs.size());
197                 }
198             });
199 
200             // Get all the fileSpecs to check for property usage.
201             // Normally **/*.java, maybe **/*.jsp, etc.
202             final Collection<String> usageFilenames = FileSpecs.getAbsoluteFilenames(usages, basedir, log)
203                     .stream().sorted().collect(Collectors.toSet());
204             // Iterate through fileSpecs and collect property usage.
205             // Iterate
206             final UsageFiles usageFiles = new UsageFiles(log);
207             if (definedPropertiesAreUsed) {
208                 log.debug("definedPropertiesAreUsed");
209                 final Map<String, String> readyTemplates = new ConcurrentHashMap<>();
210                 templates.forEach(tpl -> definedProperties.forEach(
211                         (propertyDefinition, nrPropertyDefinitions) ->
212                                 readyTemplates.put(
213                                         tpl.replaceAll(replaceInTemplateWithPropertyName, propertyDefinition),
214                                         propertyDefinition
215                                 )
216                         )
217                 );
218                 log.debug("readyTemplates:" + readyTemplates);
219                 final Collection<String> usedProperties
220                         = usageFiles.readDefinedUsagesFromFiles(usageFilenames, readyTemplates, sourceEnc);
221                 definedProperties.forEach((prop, nrOf) -> {
222                     if (!usedProperties.contains(prop)) {
223                         log.debug("Property " + prop + " not used.");
224                         propertiesNotUsed.add(prop);
225                     }
226                 });
227             }
228             if (usedPropertiesAreDefined) {
229                 log.debug("usedPropertiesAreDefined");
230                 final Set<String> readyTemplates = new HashSet<>();
231                 templates.forEach(tpl -> readyTemplates.add(
232                         tpl.replaceAll(replaceInTemplateWithPropertyName, propertyNameRegexp)
233                         )
234                 );
235                 log.debug("readyTemplates:" + readyTemplates);
236                 final Collection<UsageLocation> usageLocations
237                         = usageFiles.readAllUsagesFromFiles(usageFilenames, readyTemplates, sourceEnc);
238                 usageLocations.forEach(loc -> {
239                     if (definedProperties.containsKey(loc.getProperty())) {
240                         log.debug("Property " + loc.getProperty() + " defined.");
241                     } else {
242                         log.debug("Property " + loc.getProperty() + " not defined.");
243                         propertiesNotDefined.add(loc);
244                     }
245                 });
246             }
247         } catch (IOException e) {
248             throw new EnforcerRuleException(
249                     "IO error: " + e.getLocalizedMessage(), e
250             );
251         }
252 
253         // Report errors in wanted categories:
254         // propertiesDefinedMoreThanOnce
255         if (definitionsOnlyOnce) {
256             propertiesDefinedMoreThanOnce.forEach((key, value) ->
257                     log.error("Property '" + key + "' defined " + value + " times!")
258             );
259         }
260         // propertiesNotUsed
261         if (definedPropertiesAreUsed) {
262             propertiesNotUsed.forEach(key ->
263                     log.error("Property '" + key + "' not used!")
264             );
265         }
266         // propertiesNotDefined
267         if (usedPropertiesAreDefined) {
268             propertiesNotDefined.forEach(loc ->
269                     log.error("Property '" + loc.getProperty() + "' used without defining it ("
270                             + loc.getFilename() + ":" + loc.getRow() + ")"));
271         }
272         // reportDuplicateDefinitions
273         if (reportDuplicateDefinitions) {
274             propertiesDefined.entrySet().stream().filter(entry -> entry.getValue().size() > 1).forEach(
275                     entry -> entry.getValue().forEach(
276                             propDef -> log.info("Defined '" + propDef.getKey() + "' with value '" + propDef.getValue()
277                                     + "' in " + propDef.getFilename() + ":" + propDef.getLinenumber())
278                     )
279             );
280         }
281 
282         // Fail rule if errors in wanted categories.
283         if (definedPropertiesAreUsed && !propertiesNotUsed.isEmpty()
284                 || usedPropertiesAreDefined && !propertiesNotDefined.isEmpty()
285                 || definitionsOnlyOnce && !propertiesDefinedMoreThanOnce.isEmpty()
286         ) {
287             throw new EnforcerRuleException(
288                     "Errors in property definitions or usage!"
289             );
290         }
291     }
292 
293     private Map<String, Integer> getPropertiesDefinedMoreThanOnce(final Charset propertiesEnc, final Collection<String> propertyFilenames) throws IOException {
294         Map<String, Integer> definedProperties;
295         if (definitionsOnlyOnce) {
296             definedProperties = new PropertyFiles(log, propertiesEnc).readPropertiesFromFilesWithCount(propertyFilenames);
297             definedProperties.forEach((prop, nrOf) -> {
298                 log.debug("Property '" + prop + "' defined " + nrOf + " times.");
299                 if (nrOf > 1) {
300                     propertiesDefinedMoreThanOnce.put(prop, nrOf);
301                 }
302             });
303         } else {
304             definedProperties = new PropertyFiles(log, propertiesEnc).readPropertiesFromFilesWithoutCount(propertyFilenames);
305         }
306         return definedProperties;
307     }
308 
309     private Map<String, Set<PropertyDefinition>> getPropertiesDefined(final Charset propertiesEnc, final Collection<String> propertyFilenames) throws IOException {
310         Map<String, Set<PropertyDefinition>> definedProperties;
311         definedProperties = new PropertyFiles(log, propertiesEnc).readPropertiesFromFilesGetDefinitions(propertyFilenames);
312         return definedProperties;
313     }
314 
315 
316     /**
317      * If your rule is cacheable, you must return a unique id
318      * when parameters or conditions change that would cause
319      * the result to be different. Multiple cached results are stored
320      * based on their id.
321      * <p>
322      * The easiest way to do this is to return a hash computed
323      * from the values of your parameters.
324      * <p>
325      * If your rule is not cacheable, then the result here
326      * is not important, you may return anything.
327      *
328      * @return Always false here.
329      */
330     @Override
331     @Nullable
332     public String getCacheId() {
333         return String.valueOf(false);
334     }
335 
336     /**
337      * This tells the system if the results are cacheable at
338      * all. Keep in mind that during forked builds and other things,
339      * a given rule may be executed more than once for the same
340      * project. This means that even things that change from
341      * project to project may still be cacheable in certain instances.
342      *
343      * @return Always false here.
344      */
345     @Override
346     public boolean isCacheable() {
347         return false;
348     }
349 
350     /**
351      * If the rule is cacheable and the same id is found in the cache,
352      * the stored results are passed to this method to allow double
353      * checking of the results. Most of the time this can be done
354      * by generating unique ids, but sometimes the results of objects returned
355      * by the helper need to be queried. You may for example, store certain
356      * objects in your rule and then query them later.
357      *
358      * @param arg0 EnforcerRule
359      * @return Always false here.
360      */
361     @Override
362     public boolean isResultValid(@Nullable final EnforcerRule arg0) {
363         return false;
364     }
365 
366     /**
367      * Getters for results, used for testing.
368      */
369 
370     @Nonnull
371     public Set<String> getPropertiesNotUsed() {
372         return propertiesNotUsed;
373     }
374 
375     @Nonnull
376     public Set<UsageFiles.UsageLocation> getPropertiesNotDefined() {
377         return propertiesNotDefined;
378     }
379 
380     @Nonnull
381     public Map<String, Integer> getPropertiesDefinedMoreThanOnce() {
382         return propertiesDefinedMoreThanOnce;
383     }
384 
385     @Nonnull
386     public Map<String, Set<PropertyDefinition>> getPropertiesDefined() {
387         return propertiesDefined;
388     }
389 
390     public boolean isDefinedPropertiesAreUsed() {
391         return definedPropertiesAreUsed;
392     }
393 
394     public void setDefinedPropertiesAreUsed(final boolean definedPropertiesAreUsed) {
395         this.definedPropertiesAreUsed = definedPropertiesAreUsed;
396     }
397 
398     public boolean isUsedPropertiesAreDefined() {
399         return usedPropertiesAreDefined;
400     }
401 
402     public void setUsedPropertiesAreDefined(final boolean usedPropertiesAreDefined) {
403         this.usedPropertiesAreDefined = usedPropertiesAreDefined;
404     }
405 
406     public boolean isDefinitionsOnlyOnce() {
407         return definitionsOnlyOnce;
408     }
409 
410     public void setDefinitionsOnlyOnce(final boolean definitionsOnlyOnce) {
411         this.definitionsOnlyOnce = definitionsOnlyOnce;
412     }
413 
414     @Nonnull
415     public String getReplaceInTemplateWithPropertyName() {
416         return replaceInTemplateWithPropertyName;
417     }
418 
419     public void setReplaceInTemplateWithPropertyName(@Nonnull final String replaceInTemplateWithPropertyName) {
420         this.replaceInTemplateWithPropertyName = replaceInTemplateWithPropertyName;
421     }
422 
423     @Nonnull
424     public Collection<String> getDefinitions() {
425         return definitions;
426     }
427 
428     /**
429      * Setters for the parameters
430      * (these are not used by Maven Enforcer, used for testing).
431      */
432 
433     public void setDefinitions(@Nonnull final Collection<String> definitions) {
434         this.definitions = definitions;
435     }
436 
437     @Nonnull
438     public Collection<String> getTemplates() {
439         return templates;
440     }
441 
442     public void setTemplates(@Nonnull final Collection<String> templates) {
443         this.templates = templates;
444     }
445 
446     @Nonnull
447     public Collection<String> getUsages() {
448         return usages;
449     }
450 
451     public void setUsages(@Nonnull final Collection<String> usages) {
452         this.usages = usages;
453     }
454 }