View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.enforcer;
20  
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.stream.Collectors;
28  
29  import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
30  import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
31  import org.apache.maven.enforcer.rule.api.EnforcerLevel;
32  import org.apache.maven.enforcer.rule.api.EnforcerRule;
33  import org.apache.maven.enforcer.rule.api.EnforcerRuleBase;
34  import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
35  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
36  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
37  import org.apache.maven.execution.MavenSession;
38  import org.apache.maven.plugin.AbstractMojo;
39  import org.apache.maven.plugin.MojoExecution;
40  import org.apache.maven.plugin.MojoExecutionException;
41  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
42  import org.apache.maven.plugin.logging.Log;
43  import org.apache.maven.plugins.annotations.Component;
44  import org.apache.maven.plugins.annotations.LifecyclePhase;
45  import org.apache.maven.plugins.annotations.Mojo;
46  import org.apache.maven.plugins.annotations.Parameter;
47  import org.apache.maven.plugins.annotations.ResolutionScope;
48  import org.apache.maven.plugins.enforcer.internal.DefaultEnforcementRuleHelper;
49  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleCache;
50  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleDesc;
51  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManager;
52  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManagerException;
53  import org.apache.maven.project.MavenProject;
54  import org.codehaus.plexus.PlexusContainer;
55  import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
56  import org.codehaus.plexus.configuration.PlexusConfiguration;
57  import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
58  import org.codehaus.plexus.util.StringUtils;
59  
60  /**
61   * This goal executes the defined enforcer-rules once per module.
62   *
63   * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
64   */
65  @Mojo(
66          name = "enforce",
67          defaultPhase = LifecyclePhase.VALIDATE,
68          requiresDependencyCollection = ResolutionScope.TEST,
69          threadSafe = true)
70  public class EnforceMojo extends AbstractMojo {
71      /**
72       * This is a static variable used to persist the cached results across plugin invocations.
73       */
74      protected static Map<String, EnforcerRule> cache = new ConcurrentHashMap<>();
75  
76      /**
77       * MojoExecution needed by the ExpressionEvaluator
78       */
79      @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
80      protected MojoExecution mojoExecution;
81  
82      /**
83       * The MavenSession
84       */
85      @Parameter(defaultValue = "${session}", readonly = true, required = true)
86      protected MavenSession session;
87  
88      /**
89       * POM
90       */
91      @Parameter(defaultValue = "${project}", readonly = true, required = true)
92      protected MavenProject project;
93  
94      /**
95       * Flag to easily skip all checks
96       */
97      @Parameter(property = "enforcer.skip", defaultValue = "false")
98      protected boolean skip = false;
99  
100     /**
101      * Flag to fail the build if at least one check fails.
102      */
103     @Parameter(property = "enforcer.fail", defaultValue = "true")
104     private boolean fail = true;
105 
106     /**
107      * Fail on the first rule that doesn't pass
108      */
109     @Parameter(property = "enforcer.failFast", defaultValue = "false")
110     private boolean failFast = false;
111 
112     /**
113      * Flag to fail the build if no rules are present
114      *
115      * @since 3.2.0
116      */
117     @Parameter(property = "enforcer.failIfNoRules", defaultValue = "true")
118     private boolean failIfNoRules = true;
119 
120     /**
121      * Rules configuration to execute as XML.
122      * Each first level tag represents rule name to execute.
123      * Inner tags are configurations for rule.
124      * Eg:
125      * <pre>
126      *     &lt;rules&gt;
127      *         &lt;alwaysFail/&gt;
128      *         &lt;alwaysPass&gt;
129      *             &lt;message&gt;message for rule&lt;/message&gt;
130      *         &lt;/alwaysPass&gt;
131      *         &lt;myRule implementation="org.example.MyRule"/&gt;
132      *     &lt;/rules&gt;
133      * </pre>
134      *
135      * @since 1.0.0
136      */
137     @Parameter
138     private PlexusConfiguration rules;
139 
140     /**
141      * List of strings that matches the EnforcerRules to skip.
142      *
143      * @since 3.2.0
144      */
145     @Parameter(required = false, property = "enforcer.skipRules")
146     private List<String> rulesToSkip;
147 
148     /**
149      * Use this flag to disable rule result caching. This will cause all rules to execute on each project even if the
150      * rule indicates it can safely be cached.
151      */
152     @Parameter(property = "enforcer.ignoreCache", defaultValue = "false")
153     protected boolean ignoreCache = false;
154 
155     @Component
156     private PlexusContainer container;
157 
158     @Component
159     private EnforcerRuleManager enforcerRuleManager;
160 
161     @Component
162     private EnforcerRuleCache ruleCache;
163 
164     private List<String> rulesToExecute;
165 
166     /**
167      * List of strings that matches the EnforcerRules to execute. Replacement for the <code>rules</code> property.
168      *
169      * @param rulesToExecute a rules to execute
170      * @throws MojoExecutionException when values are incorrect
171      * @since 3.2.0
172      */
173     @Parameter(required = false, property = "enforcer.rules")
174     public void setRulesToExecute(List<String> rulesToExecute) throws MojoExecutionException {
175         if (rulesToExecute != null && !rulesToExecute.isEmpty()) {
176             if (this.rulesToExecute != null && !this.rulesToExecute.isEmpty()) {
177                 throw new MojoExecutionException("Detected the usage of both '-Drules' (which is deprecated) "
178                         + "and '-Denforcer.rules'. Please use only one of them, preferably '-Denforcer.rules'.");
179             }
180             this.rulesToExecute = rulesToExecute;
181         }
182     }
183 
184     /**
185      * List of strings that matches the EnforcerRules to execute.
186      *
187      * @param rulesToExecute a rules to execute
188      * @throws MojoExecutionException when values are incorrect
189      * @deprecated Use <code>enforcer.rules</code> property instead
190      */
191     @Parameter(required = false, property = "rules")
192     @Deprecated
193     public void setCommandLineRules(List<String> rulesToExecute) throws MojoExecutionException {
194         if (rulesToExecute != null && !rulesToExecute.isEmpty()) {
195             getLog().warn(
196                             "Detected the usage of property '-Drules' which is deprecated. Use '-Denforcer.rules' instead.");
197         }
198         setRulesToExecute(rulesToExecute);
199     }
200 
201     @Override
202     public void execute() throws MojoExecutionException {
203         Log log = this.getLog();
204 
205         if (skip) {
206             log.info("Skipping Rule Enforcement.");
207             return;
208         }
209 
210         Optional<PlexusConfiguration> rulesFromCommandLine = createRulesFromCommandLineOptions();
211         List<EnforcerRuleDesc> rulesList;
212 
213         // current behavior - rules from command line override all other configured rules.
214         List<EnforcerRuleDesc> allRules = enforcerRuleManager.createRules(rulesFromCommandLine.orElse(rules), log);
215         rulesList = filterOutSkippedRules(allRules);
216 
217         List<EnforcerRuleDesc> additionalRules = processRuleConfigProviders(rulesList);
218         rulesList = filterOutRuleConfigProviders(rulesList);
219         rulesList.addAll(additionalRules);
220 
221         if (rulesList.isEmpty()) {
222             if (failIfNoRules) {
223                 throw new MojoExecutionException(
224                         "No rules are configured. Use the skip flag if you want to disable execution.");
225             } else {
226                 log.warn("No rules are configured.");
227                 return;
228             }
229         }
230 
231         // create my helper
232         PluginParameterExpressionEvaluator evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution);
233         EnforcerRuleHelper helper = new DefaultEnforcementRuleHelper(session, evaluator, log, container);
234 
235         // if we are only warning, then disable
236         // failFast
237         if (!fail) {
238             failFast = false;
239         }
240 
241         List<String> errorMessages = new ArrayList<>();
242 
243         // go through each rule
244         for (int ruleIndex = 0; ruleIndex < rulesList.size(); ruleIndex++) {
245 
246             EnforcerRuleDesc ruleDesc = rulesList.get(ruleIndex);
247             EnforcerLevel level = ruleDesc.getLevel();
248             try {
249                 executeRule(ruleIndex, ruleDesc, helper);
250             } catch (EnforcerRuleError e) {
251                 String ruleMessage = createRuleMessage(ruleIndex, ruleDesc, EnforcerLevel.ERROR, e);
252                 throw new MojoExecutionException(System.lineSeparator() + ruleMessage, e);
253             } catch (EnforcerRuleException e) {
254 
255                 String ruleMessage = createRuleMessage(ruleIndex, ruleDesc, level, e);
256 
257                 if (failFast && level == EnforcerLevel.ERROR) {
258                     throw new MojoExecutionException(System.lineSeparator() + ruleMessage, e);
259                 }
260 
261                 if (level == EnforcerLevel.ERROR) {
262                     errorMessages.add(ruleMessage);
263                 } else {
264                     log.warn(ruleMessage);
265                 }
266             }
267         }
268 
269         if (!errorMessages.isEmpty()) {
270             if (fail) {
271                 throw new MojoExecutionException(
272                         System.lineSeparator() + String.join(System.lineSeparator(), errorMessages));
273             } else {
274                 errorMessages.forEach(log::warn);
275             }
276         }
277     }
278 
279     private List<EnforcerRuleDesc> processRuleConfigProviders(List<EnforcerRuleDesc> rulesList) {
280         return rulesList.stream()
281                 .filter(Objects::nonNull)
282                 .filter(rd -> rd.getRule() instanceof AbstractEnforcerRuleConfigProvider)
283                 .map(this::executeRuleConfigProvider)
284                 .flatMap(xml -> enforcerRuleManager.createRules(xml, getLog()).stream())
285                 .collect(Collectors.toList());
286     }
287 
288     private List<EnforcerRuleDesc> filterOutRuleConfigProviders(List<EnforcerRuleDesc> rulesList) {
289         return rulesList.stream()
290                 .filter(Objects::nonNull)
291                 .filter(rd -> !(rd.getRule() instanceof AbstractEnforcerRuleConfigProvider))
292                 .collect(Collectors.toList());
293     }
294 
295     private XmlPlexusConfiguration executeRuleConfigProvider(EnforcerRuleDesc ruleDesc) {
296         AbstractEnforcerRuleConfigProvider ruleProducer = (AbstractEnforcerRuleConfigProvider) ruleDesc.getRule();
297 
298         if (getLog().isDebugEnabled()) {
299             getLog().debug(String.format("Executing Rule Config Provider %s", ruleDesc.getRule()));
300         }
301 
302         try {
303             XmlPlexusConfiguration configuration = new XmlPlexusConfiguration(ruleProducer.getRulesConfig());
304             getLog().info(String.format("Rule Config Provider %s executed", getRuleName(ruleDesc)));
305 
306             return configuration;
307         } catch (EnforcerRuleException e) {
308             throw new EnforcerRuleManagerException("Rules Provider error for: " + getRuleName(ruleDesc), e);
309         }
310     }
311 
312     private void executeRule(int ruleIndex, EnforcerRuleDesc ruleDesc, EnforcerRuleHelper helper)
313             throws EnforcerRuleException {
314 
315         if (getLog().isDebugEnabled()) {
316             getLog().debug(String.format("Executing Rule %d: %s", ruleIndex, ruleDesc));
317         }
318 
319         long startTime = System.currentTimeMillis();
320 
321         try {
322             if (ruleDesc.getRule() instanceof EnforcerRule) {
323                 executeRuleOld(ruleIndex, ruleDesc, helper);
324             } else if (ruleDesc.getRule() instanceof AbstractEnforcerRule) {
325                 executeRuleNew(ruleIndex, ruleDesc);
326             }
327         } finally {
328             if (getLog().isDebugEnabled()) {
329                 long workTime = System.currentTimeMillis() - startTime;
330                 getLog().debug(String.format(
331                         "Finish Rule %d: %s takes %d ms", ruleIndex, getRuleName(ruleDesc), workTime));
332             }
333         }
334     }
335 
336     private void executeRuleOld(int ruleIndex, EnforcerRuleDesc ruleDesc, EnforcerRuleHelper helper)
337             throws EnforcerRuleException {
338 
339         EnforcerRule rule = (EnforcerRule) ruleDesc.getRule();
340 
341         if (ignoreCache || shouldExecute(rule)) {
342             rule.execute(helper);
343             getLog().info(String.format("Rule %d: %s passed", ruleIndex, getRuleName(ruleDesc)));
344         }
345     }
346 
347     private void executeRuleNew(int ruleIndex, EnforcerRuleDesc ruleDesc) throws EnforcerRuleException {
348 
349         AbstractEnforcerRule rule = (AbstractEnforcerRule) ruleDesc.getRule();
350         if (ignoreCache || !ruleCache.isCached(rule)) {
351             rule.execute();
352             getLog().info(String.format("Rule %d: %s passed", ruleIndex, getRuleName(ruleDesc)));
353         }
354     }
355 
356     /**
357      * Create rules configuration based on command line provided rules list.
358      *
359      * @return a configuration in case where rules list is present or empty
360      */
361     private Optional<PlexusConfiguration> createRulesFromCommandLineOptions() {
362 
363         if (rulesToExecute == null || rulesToExecute.isEmpty()) {
364             return Optional.empty();
365         }
366 
367         PlexusConfiguration configuration = new DefaultPlexusConfiguration("rules");
368         for (String rule : rulesToExecute) {
369             PlexusConfiguration configuredRule = null;
370             // Check if there's configuration in the project for this rule and use it if so
371             if (rules != null) {
372                 // rule names haven't been normalized yet, so check both with first character upper and lower
373                 String ruleLower = Character.toLowerCase(rule.charAt(0)) + rule.substring(1);
374                 String ruleUpper = Character.toUpperCase(rule.charAt(0)) + rule.substring(1);
375                 configuredRule = rules.getChild(ruleLower, false);
376                 if (configuredRule == null) {
377                     configuredRule = rules.getChild(ruleUpper, false);
378                 }
379             }
380 
381             if (configuredRule != null) {
382                 configuration.addChild(configuredRule);
383             } else {
384                 configuration.addChild(new DefaultPlexusConfiguration(rule));
385             }
386         }
387         return Optional.of(configuration);
388     }
389 
390     /**
391      * Filter out (remove) rules that have been specifically skipped via additional configuration.
392      *
393      * @param allRules list of enforcer rules to go through and filter
394      * @return list of filtered rules
395      */
396     private List<EnforcerRuleDesc> filterOutSkippedRules(List<EnforcerRuleDesc> allRules) {
397         if (rulesToSkip == null || rulesToSkip.isEmpty()) {
398             return allRules;
399         }
400         return allRules.stream()
401                 .filter(ruleDesc -> !rulesToSkip.contains(ruleDesc.getName()))
402                 .collect(Collectors.toList());
403     }
404 
405     /**
406      * This method determines if a rule should execute based on the cache
407      *
408      * @param rule the rule to verify
409      * @return {@code true} if rule should be executed, otherwise {@code false}
410      */
411     protected boolean shouldExecute(EnforcerRule rule) {
412         if (rule.isCacheable()) {
413             Log log = this.getLog();
414             log.debug("Rule " + rule.getClass().getName() + " is cacheable.");
415             String key = rule.getClass().getName() + " " + rule.getCacheId();
416             if (EnforceMojo.cache.containsKey(key)) {
417                 log.debug("Key " + key + " was found in the cache");
418                 if (rule.isResultValid(cache.get(key))) {
419                     log.debug("The cached results are still valid. Skipping the rule: "
420                             + rule.getClass().getName());
421                     return false;
422                 }
423             }
424 
425             // add it to the cache of executed rules
426             EnforceMojo.cache.put(key, rule);
427         }
428         return true;
429     }
430 
431     /**
432      * Set rule list to skip.
433      *
434      * @param rulesToSkip a rule list
435      */
436     public void setRulesToSkip(List<String> rulesToSkip) {
437         if (rulesToSkip == null) {
438             return;
439         }
440         // internally all rules begin from lowercase letter
441         this.rulesToSkip = rulesToSkip.stream()
442                 .filter(Objects::nonNull)
443                 .map(StringUtils::lowercaseFirstLetter)
444                 .collect(Collectors.toList());
445     }
446 
447     /**
448      * @param fail whether to fail
449      */
450     public void setFail(boolean fail) {
451         this.fail = fail;
452     }
453 
454     /**
455      * @param failFast whether to fail fast
456      */
457     public void setFailFast(boolean failFast) {
458         this.failFast = failFast;
459     }
460 
461     private String createRuleMessage(
462             int ruleIndex, EnforcerRuleDesc ruleDesc, EnforcerLevel level, EnforcerRuleException e) {
463 
464         StringBuilder result = new StringBuilder();
465         result.append("Rule ").append(ruleIndex).append(": ").append(getRuleName(ruleDesc));
466 
467         if (level == EnforcerLevel.ERROR) {
468             result.append(" failed");
469         } else {
470             result.append(" warned");
471         }
472 
473         if (e.getMessage() != null) {
474             result.append(" with message:").append(System.lineSeparator()).append(e.getMessage());
475         } else {
476             result.append(" without a message");
477         }
478 
479         return result.toString();
480     }
481 
482     private String getRuleName(EnforcerRuleDesc ruleDesc) {
483 
484         Class<? extends EnforcerRuleBase> ruleClass = ruleDesc.getRule().getClass();
485 
486         String ruleName = ruleClass.getName();
487 
488         if (!ruleClass.getSimpleName().equalsIgnoreCase(ruleDesc.getName())) {
489             ruleName += "(" + ruleDesc.getName() + ")";
490         }
491 
492         return ruleName;
493     }
494 
495     /**
496      * @param failIfNoRules whether to fail if there are no rules
497      */
498     public void setFailIfNoRules(boolean failIfNoRules) {
499         this.failIfNoRules = failIfNoRules;
500     }
501 }