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.Hashtable;
23  import java.util.List;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.stream.Collectors;
27  
28  import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
29  import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
30  import org.apache.maven.enforcer.rule.api.EnforcerLevel;
31  import org.apache.maven.enforcer.rule.api.EnforcerRule;
32  import org.apache.maven.enforcer.rule.api.EnforcerRuleBase;
33  import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
34  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
35  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
36  import org.apache.maven.execution.MavenSession;
37  import org.apache.maven.plugin.AbstractMojo;
38  import org.apache.maven.plugin.MojoExecution;
39  import org.apache.maven.plugin.MojoExecutionException;
40  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
41  import org.apache.maven.plugin.logging.Log;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.LifecyclePhase;
44  import org.apache.maven.plugins.annotations.Mojo;
45  import org.apache.maven.plugins.annotations.Parameter;
46  import org.apache.maven.plugins.annotations.ResolutionScope;
47  import org.apache.maven.plugins.enforcer.internal.DefaultEnforcementRuleHelper;
48  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleCache;
49  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleDesc;
50  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManager;
51  import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManagerException;
52  import org.apache.maven.project.MavenProject;
53  import org.codehaus.plexus.PlexusContainer;
54  import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
55  import org.codehaus.plexus.configuration.PlexusConfiguration;
56  import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
57  import org.codehaus.plexus.util.StringUtils;
58  
59  /**
60   * This goal executes the defined enforcer-rules once per module.
61   *
62   * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
63   */
64  @Mojo(
65          name = "enforce",
66          defaultPhase = LifecyclePhase.VALIDATE,
67          requiresDependencyCollection = ResolutionScope.TEST,
68          threadSafe = true)
69  public class EnforceMojo extends AbstractMojo {
70      /**
71       * This is a static variable used to persist the cached results across plugin invocations.
72       */
73      protected static Hashtable<String, EnforcerRule> cache = new Hashtable<>();
74  
75      /**
76       * MojoExecution needed by the ExpressionEvaluator
77       */
78      @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
79      protected MojoExecution mojoExecution;
80  
81      /**
82       * The MavenSession
83       */
84      @Parameter(defaultValue = "${session}", readonly = true, required = true)
85      protected MavenSession session;
86  
87      /**
88       * POM
89       */
90      @Parameter(defaultValue = "${project}", readonly = true, required = true)
91      protected MavenProject project;
92  
93      /**
94       * Flag to easily skip all checks
95       */
96      @Parameter(property = "enforcer.skip", defaultValue = "false")
97      protected boolean skip = false;
98  
99      /**
100      * Flag to fail the build if at least one check fails.
101      */
102     @Parameter(property = "enforcer.fail", defaultValue = "true")
103     private boolean fail = true;
104 
105     /**
106      * Fail on the first rule that doesn't pass
107      */
108     @Parameter(property = "enforcer.failFast", defaultValue = "false")
109     private boolean failFast = false;
110 
111     /**
112      * Flag to fail the build if no rules are present
113      *
114      * @since 3.2.0
115      */
116     @Parameter(property = "enforcer.failIfNoRules", defaultValue = "true")
117     private boolean failIfNoRules = true;
118 
119     /**
120      * Rules configuration to execute as XML.
121      * Each first level tag represents rule name to execute.
122      * Inner tags are configurations for rule.
123      * Eg:
124      * <pre>
125      *     &lt;rules&gt;
126      *         &lt;alwaysFail/&gt;
127      *         &lt;alwaysPass&gt;
128      *             &lt;message&gt;message for rule&lt;/message&gt;
129      *         &lt;/alwaysPass&gt;
130      *         &lt;myRule implementation="org.example.MyRule"/&gt;
131      *     &lt;/rules&gt;
132      * </pre>
133      *
134      * @since 1.0.0
135      */
136     @Parameter
137     private PlexusConfiguration rules;
138 
139     /**
140      * List of strings that matches the EnforcerRules to skip.
141      *
142      * @since 3.2.0
143      */
144     @Parameter(required = false, property = "enforcer.skipRules")
145     private List<String> rulesToSkip;
146 
147     /**
148      * Use this flag to disable rule result caching. This will cause all rules to execute on each project even if the
149      * rule indicates it can safely be cached.
150      */
151     @Parameter(property = "enforcer.ignoreCache", defaultValue = "false")
152     protected boolean ignoreCache = false;
153 
154     @Component
155     private PlexusContainer container;
156 
157     @Component
158     private EnforcerRuleManager enforcerRuleManager;
159 
160     @Component
161     private EnforcerRuleCache ruleCache;
162 
163     private List<String> rulesToExecute;
164 
165     /**
166      * List of strings that matches the EnforcerRules to execute. Replacement for the <code>rules</code> property.
167      *
168      * @param rulesToExecute a rules to execute
169      * @throws MojoExecutionException when values are incorrect
170      * @since 3.2.0
171      */
172     @Parameter(required = false, property = "enforcer.rules")
173     public void setRulesToExecute(List<String> rulesToExecute) throws MojoExecutionException {
174         if (rulesToExecute != null && !rulesToExecute.isEmpty()) {
175             if (this.rulesToExecute != null && !this.rulesToExecute.isEmpty()) {
176                 throw new MojoExecutionException("Detected the usage of both '-Drules' (which is deprecated) "
177                         + "and '-Denforcer.rules'. Please use only one of them, preferably '-Denforcer.rules'.");
178             }
179             this.rulesToExecute = rulesToExecute;
180         }
181     }
182 
183     /**
184      * List of strings that matches the EnforcerRules to execute.
185      *
186      * @param rulesToExecute a rules to execute
187      * @throws MojoExecutionException when values are incorrect
188      * @deprecated Use <code>enforcer.rules</code> property instead
189      */
190     @Parameter(required = false, property = "rules")
191     @Deprecated
192     public void setCommandLineRules(List<String> rulesToExecute) throws MojoExecutionException {
193         if (rulesToExecute != null && !rulesToExecute.isEmpty()) {
194             getLog().warn(
195                             "Detected the usage of property '-Drules' which is deprecated. Use '-Denforcer.rules' instead.");
196         }
197         setRulesToExecute(rulesToExecute);
198     }
199 
200     @Override
201     public void execute() throws MojoExecutionException {
202         Log log = this.getLog();
203 
204         if (skip) {
205             log.info("Skipping Rule Enforcement.");
206             return;
207         }
208 
209         Optional<PlexusConfiguration> rulesFromCommandLine = createRulesFromCommandLineOptions();
210         List<EnforcerRuleDesc> rulesList;
211 
212         // current behavior - rules from command line override all other configured rules.
213         List<EnforcerRuleDesc> allRules = enforcerRuleManager.createRules(rulesFromCommandLine.orElse(rules), log);
214         rulesList = filterOutSkippedRules(allRules);
215 
216         List<EnforcerRuleDesc> additionalRules = processRuleConfigProviders(rulesList);
217         rulesList = filterOutRuleConfigProviders(rulesList);
218         rulesList.addAll(additionalRules);
219 
220         if (rulesList.isEmpty()) {
221             if (failIfNoRules) {
222                 throw new MojoExecutionException(
223                         "No rules are configured. Use the skip flag if you want to disable execution.");
224             } else {
225                 log.warn("No rules are configured.");
226                 return;
227             }
228         }
229 
230         // create my helper
231         PluginParameterExpressionEvaluator evaluator = new PluginParameterExpressionEvaluator(session, mojoExecution);
232         EnforcerRuleHelper helper = new DefaultEnforcementRuleHelper(session, evaluator, log, container);
233 
234         // if we are only warning, then disable
235         // failFast
236         if (!fail) {
237             failFast = false;
238         }
239 
240         List<String> errorMessages = new ArrayList<>();
241 
242         // go through each rule
243         for (int ruleIndex = 0; ruleIndex < rulesList.size(); ruleIndex++) {
244 
245             EnforcerRuleDesc ruleDesc = rulesList.get(ruleIndex);
246             EnforcerLevel level = ruleDesc.getLevel();
247             try {
248                 executeRule(ruleIndex, ruleDesc, helper);
249             } catch (EnforcerRuleError e) {
250                 String ruleMessage = createRuleMessage(ruleIndex, ruleDesc, EnforcerLevel.ERROR, e);
251                 throw new MojoExecutionException(System.lineSeparator() + ruleMessage, e);
252             } catch (EnforcerRuleException e) {
253 
254                 String ruleMessage = createRuleMessage(ruleIndex, ruleDesc, level, e);
255 
256                 if (failFast && level == EnforcerLevel.ERROR) {
257                     throw new MojoExecutionException(System.lineSeparator() + ruleMessage, e);
258                 }
259 
260                 if (level == EnforcerLevel.ERROR) {
261                     errorMessages.add(ruleMessage);
262                 } else {
263                     log.warn(ruleMessage);
264                 }
265             }
266         }
267 
268         if (!errorMessages.isEmpty()) {
269             if (fail) {
270                 throw new MojoExecutionException(
271                         System.lineSeparator() + String.join(System.lineSeparator(), errorMessages));
272             } else {
273                 errorMessages.forEach(log::warn);
274             }
275         }
276     }
277 
278     private List<EnforcerRuleDesc> processRuleConfigProviders(List<EnforcerRuleDesc> rulesList) {
279         return rulesList.stream()
280                 .filter(Objects::nonNull)
281                 .filter(rd -> rd.getRule() instanceof AbstractEnforcerRuleConfigProvider)
282                 .map(this::executeRuleConfigProvider)
283                 .flatMap(xml -> enforcerRuleManager.createRules(xml, getLog()).stream())
284                 .collect(Collectors.toList());
285     }
286 
287     private List<EnforcerRuleDesc> filterOutRuleConfigProviders(List<EnforcerRuleDesc> rulesList) {
288         return rulesList.stream()
289                 .filter(Objects::nonNull)
290                 .filter(rd -> !(rd.getRule() instanceof AbstractEnforcerRuleConfigProvider))
291                 .collect(Collectors.toList());
292     }
293 
294     private XmlPlexusConfiguration executeRuleConfigProvider(EnforcerRuleDesc ruleDesc) {
295         AbstractEnforcerRuleConfigProvider ruleProducer = (AbstractEnforcerRuleConfigProvider) ruleDesc.getRule();
296 
297         if (getLog().isDebugEnabled()) {
298             getLog().debug(String.format("Executing Rule Config Provider %s", ruleDesc.getRule()));
299         }
300 
301         XmlPlexusConfiguration configuration = null;
302         try {
303             configuration = new XmlPlexusConfiguration(ruleProducer.getRulesConfig());
304         } catch (EnforcerRuleException e) {
305             throw new EnforcerRuleManagerException("Rules Provider error for: " + getRuleName(ruleDesc), e);
306         }
307         getLog().info(String.format("Rule Config Provider %s executed", getRuleName(ruleDesc)));
308 
309         return configuration;
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 an 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             configuration.addChild(new DefaultPlexusConfiguration(rule));
370         }
371         return Optional.of(configuration);
372     }
373 
374     /**
375      * Filter out (remove) rules that have been specifically skipped via additional configuration.
376      *
377      * @param allRules list of enforcer rules to go through and filter
378      * @return list of filtered rules
379      */
380     private List<EnforcerRuleDesc> filterOutSkippedRules(List<EnforcerRuleDesc> allRules) {
381         if (rulesToSkip == null || rulesToSkip.isEmpty()) {
382             return allRules;
383         }
384         return allRules.stream()
385                 .filter(ruleDesc -> !rulesToSkip.contains(ruleDesc.getName()))
386                 .collect(Collectors.toList());
387     }
388 
389     /**
390      * This method determines if a rule should execute based on the cache
391      *
392      * @param rule the rule to verify
393      * @return {@code true} if rule should be executed, otherwise {@code false}
394      */
395     protected boolean shouldExecute(EnforcerRule rule) {
396         if (rule.isCacheable()) {
397             Log log = this.getLog();
398             log.debug("Rule " + rule.getClass().getName() + " is cacheable.");
399             String key = rule.getClass().getName() + " " + rule.getCacheId();
400             if (EnforceMojo.cache.containsKey(key)) {
401                 log.debug("Key " + key + " was found in the cache");
402                 if (rule.isResultValid(cache.get(key))) {
403                     log.debug("The cached results are still valid. Skipping the rule: "
404                             + rule.getClass().getName());
405                     return false;
406                 }
407             }
408 
409             // add it to the cache of executed rules
410             EnforceMojo.cache.put(key, rule);
411         }
412         return true;
413     }
414 
415     /**
416      * Set rule list to skip.
417      *
418      * @param rulesToSkip a rule list
419      */
420     public void setRulesToSkip(List<String> rulesToSkip) {
421         if (rulesToSkip == null) {
422             return;
423         }
424         // internally all rules begin from lowercase letter
425         this.rulesToSkip = rulesToSkip.stream()
426                 .filter(Objects::nonNull)
427                 .map(StringUtils::lowercaseFirstLetter)
428                 .collect(Collectors.toList());
429     }
430 
431     /**
432      * @param theFail the fail to set
433      */
434     public void setFail(boolean theFail) {
435         this.fail = theFail;
436     }
437 
438     /**
439      * @param theFailFast the failFast to set
440      */
441     public void setFailFast(boolean theFailFast) {
442         this.failFast = theFailFast;
443     }
444 
445     private String createRuleMessage(
446             int ruleIndex, EnforcerRuleDesc ruleDesc, EnforcerLevel level, EnforcerRuleException e) {
447 
448         StringBuilder result = new StringBuilder();
449         result.append("Rule ").append(ruleIndex).append(": ").append(getRuleName(ruleDesc));
450 
451         if (level == EnforcerLevel.ERROR) {
452             result.append(" failed");
453         } else {
454             result.append(" warned");
455         }
456 
457         if (e.getMessage() != null) {
458             result.append(" with message:").append(System.lineSeparator()).append(e.getMessage());
459         } else {
460             result.append(" without a message");
461         }
462 
463         return result.toString();
464     }
465 
466     private String getRuleName(EnforcerRuleDesc ruleDesc) {
467 
468         Class<? extends EnforcerRuleBase> ruleClass = ruleDesc.getRule().getClass();
469 
470         String ruleName = ruleClass.getName();
471 
472         if (!ruleClass.getSimpleName().equalsIgnoreCase(ruleDesc.getName())) {
473             ruleName += "(" + ruleDesc.getName() + ")";
474         }
475 
476         return ruleName;
477     }
478 
479     /**
480      * @param thefailIfNoRules the failIfNoRules to set
481      */
482     public void setFailIfNoRules(boolean thefailIfNoRules) {
483         this.failIfNoRules = thefailIfNoRules;
484     }
485 }