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 * <rules> 127 * <alwaysFail/> 128 * <alwaysPass> 129 * <message>message for rule</message> 130 * </alwaysPass> 131 * <myRule implementation="org.example.MyRule"/> 132 * </rules> 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}