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.Hashtable;
023import java.util.List;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.stream.Collectors;
027
028import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
029import org.apache.maven.enforcer.rule.api.AbstractEnforcerRuleConfigProvider;
030import org.apache.maven.enforcer.rule.api.EnforcerLevel;
031import org.apache.maven.enforcer.rule.api.EnforcerRule;
032import org.apache.maven.enforcer.rule.api.EnforcerRuleBase;
033import org.apache.maven.enforcer.rule.api.EnforcerRuleError;
034import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
035import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
036import org.apache.maven.execution.MavenSession;
037import org.apache.maven.plugin.AbstractMojo;
038import org.apache.maven.plugin.MojoExecution;
039import org.apache.maven.plugin.MojoExecutionException;
040import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
041import org.apache.maven.plugin.logging.Log;
042import org.apache.maven.plugins.annotations.Component;
043import org.apache.maven.plugins.annotations.LifecyclePhase;
044import org.apache.maven.plugins.annotations.Mojo;
045import org.apache.maven.plugins.annotations.Parameter;
046import org.apache.maven.plugins.annotations.ResolutionScope;
047import org.apache.maven.plugins.enforcer.internal.DefaultEnforcementRuleHelper;
048import org.apache.maven.plugins.enforcer.internal.EnforcerRuleCache;
049import org.apache.maven.plugins.enforcer.internal.EnforcerRuleDesc;
050import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManager;
051import org.apache.maven.plugins.enforcer.internal.EnforcerRuleManagerException;
052import org.apache.maven.project.MavenProject;
053import org.codehaus.plexus.PlexusContainer;
054import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
055import org.codehaus.plexus.configuration.PlexusConfiguration;
056import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
057import org.codehaus.plexus.util.StringUtils;
058
059/**
060 * This goal executes the defined enforcer-rules once per module.
061 *
062 * @author <a href="mailto:brianf@apache.org">Brian Fox</a>
063 */
064@Mojo(
065        name = "enforce",
066        defaultPhase = LifecyclePhase.VALIDATE,
067        requiresDependencyCollection = ResolutionScope.TEST,
068        threadSafe = true)
069public class EnforceMojo extends AbstractMojo {
070    /**
071     * This is a static variable used to persist the cached results across plugin invocations.
072     */
073    protected static Hashtable<String, EnforcerRule> cache = new Hashtable<>();
074
075    /**
076     * MojoExecution needed by the ExpressionEvaluator
077     */
078    @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
079    protected MojoExecution mojoExecution;
080
081    /**
082     * The MavenSession
083     */
084    @Parameter(defaultValue = "${session}", readonly = true, required = true)
085    protected MavenSession session;
086
087    /**
088     * POM
089     */
090    @Parameter(defaultValue = "${project}", readonly = true, required = true)
091    protected MavenProject project;
092
093    /**
094     * Flag to easily skip all checks
095     */
096    @Parameter(property = "enforcer.skip", defaultValue = "false")
097    protected boolean skip = false;
098
099    /**
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}