001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.plugins.enforcer;
020
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.stream.Collectors;
028
029import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
030import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
031import org.apache.maven.enforcer.rule.api.EnforcerLevel;
032import org.apache.maven.enforcer.rule.api.EnforcerRule;
033import org.apache.maven.enforcer.rule.api.EnforcerRuleBase;
034import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
035import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
036import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
037import org.apache.maven.execution.MavenSession;
038import org.apache.maven.plugin.AbstractMojo;
039import org.apache.maven.plugin.MojoExecution;
040import org.apache.maven.plugin.MojoExecutionException;
041import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
042import org.apache.maven.plugin.logging.Log;
043import org.apache.maven.plugins.annotations.Component;
044import org.apache.maven.plugins.annotations.LifecyclePhase;
045import org.apache.maven.plugins.annotations.Mojo;
046import org.apache.maven.plugins.annotations.Parameter;
047import org.apache.maven.plugins.annotations.ResolutionScope;
048import org.apache.maven.plugins.enforcer.internal.DefaultEnforcementRuleHelper;
049import org.apache.maven.plugins.enforcer.internal.EnforcerRuleCache;
050import org.apache.maven.plugins.enforcer.internal.EnforcerRuleDesc;
051import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManager;
052import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManagerException;
053import org.apache.maven.project.MavenProject;
054import org.codehaus.plexus.PlexusContainer;
055import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
056import org.codehaus.plexus.configuration.PlexusConfiguration;
057import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
058import org.codehaus.plexus.util.StringUtils;
059
060/**
061 * This goal executes the defined enforcer-rules once per module.
062 *
063 * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
064 */
065@Mojo(
066        name = "enforce",
067        defaultPhase = LifecyclePhase.VALIDATE,
068        requiresDependencyCollection = ResolutionScope.TEST,
069        threadSafe = true)
070public class EnforceMojo extends AbstractMojo {
071    /**
072     * This is a static variable used to persist the cached results across plugin invocations.
073     */
074    protected static Map<String, EnforcerRule> cache = new ConcurrentHashMap<>();
075
076    /**
077     * MojoExecution needed by the ExpressionEvaluator
078     */
079    @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
080    protected MojoExecution mojoExecution;
081
082    /**
083     * The MavenSession
084     */
085    @Parameter(defaultValue = "${session}", readonly = true, required = true)
086    protected MavenSession session;
087
088    /**
089     * POM
090     */
091    @Parameter(defaultValue = "${project}", readonly = true, required = true)
092    protected MavenProject project;
093
094    /**
095     * Flag to easily skip all checks
096     */
097    @Parameter(property = "enforcer.skip", defaultValue = "false")
098    protected boolean skip = false;
099
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}