001package org.apache.maven.model.validation; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.File; 023import java.util.Arrays; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.regex.Pattern; 030 031import org.apache.maven.model.Activation; 032import org.apache.maven.model.ActivationFile; 033import org.apache.maven.model.Build; 034import org.apache.maven.model.BuildBase; 035import org.apache.maven.model.Dependency; 036import org.apache.maven.model.DependencyManagement; 037import org.apache.maven.model.DistributionManagement; 038import org.apache.maven.model.Exclusion; 039import org.apache.maven.model.InputLocation; 040import org.apache.maven.model.InputLocationTracker; 041import org.apache.maven.model.Model; 042import org.apache.maven.model.Parent; 043import org.apache.maven.model.Plugin; 044import org.apache.maven.model.PluginExecution; 045import org.apache.maven.model.PluginManagement; 046import org.apache.maven.model.Profile; 047import org.apache.maven.model.ReportPlugin; 048import org.apache.maven.model.Reporting; 049import org.apache.maven.model.Repository; 050import org.apache.maven.model.Resource; 051import org.apache.maven.model.building.ModelBuildingRequest; 052import org.apache.maven.model.building.ModelProblem.Severity; 053import org.apache.maven.model.building.ModelProblem.Version; 054import org.apache.maven.model.building.ModelProblemCollector; 055import org.apache.maven.model.building.ModelProblemCollectorRequest; 056import org.codehaus.plexus.component.annotations.Component; 057import org.codehaus.plexus.util.StringUtils; 058 059/** 060 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl</a> 061 */ 062@Component( role = ModelValidator.class ) 063public class DefaultModelValidator 064 implements ModelValidator 065{ 066 067 private static final Pattern ID_REGEX = Pattern.compile( "[A-Za-z0-9_\\-.]+" ); 068 069 private static final Pattern ID_WITH_WILDCARDS_REGEX = Pattern.compile( "[A-Za-z0-9_\\-.?*]+" ); 070 071 private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*"; 072 073 private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS; 074 075 private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS; 076 077 public void validateRawModel( Model model, ModelBuildingRequest request, ModelProblemCollector problems ) 078 { 079 Parent parent = model.getParent(); 080 if ( parent != null ) 081 { 082 validateStringNotEmpty( "parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), 083 parent ); 084 085 validateStringNotEmpty( "parent.artifactId", problems, Severity.FATAL, Version.BASE, 086 parent.getArtifactId(), parent ); 087 088 validateStringNotEmpty( "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), 089 parent ); 090 091 if ( equals( parent.getGroupId(), model.getGroupId() ) 092 && equals( parent.getArtifactId(), model.getArtifactId() ) ) 093 { 094 addViolation( problems, Severity.FATAL, Version.BASE, "parent.artifactId", null, "must be changed" 095 + ", the parent element cannot have the same groupId:artifactId as the project.", parent ); 096 } 097 } 098 099 if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) 100 { 101 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 102 103 validateEnum( "modelVersion", problems, Severity.ERROR, Version.V20, model.getModelVersion(), null, model, 104 "4.0.0" ); 105 106 validateStringNoExpression( "groupId", problems, Severity.WARNING, Version.V20, model.getGroupId(), model ); 107 if ( parent == null ) 108 { 109 validateStringNotEmpty( "groupId", problems, Severity.FATAL, Version.V20, model.getGroupId(), model ); 110 } 111 112 validateStringNoExpression( "artifactId", problems, Severity.WARNING, Version.V20, model.getArtifactId(), 113 model ); 114 validateStringNotEmpty( "artifactId", problems, Severity.FATAL, Version.V20, model.getArtifactId(), model ); 115 116 validateVersionNoExpression( "version", problems, Severity.WARNING, Version.V20, model.getVersion(), model ); 117 if ( parent == null ) 118 { 119 validateStringNotEmpty( "version", problems, Severity.FATAL, Version.V20, model.getVersion(), model ); 120 } 121 122 validate20RawDependencies( problems, model.getDependencies(), "dependencies.dependency", request ); 123 124 if ( model.getDependencyManagement() != null ) 125 { 126 validate20RawDependencies( problems, model.getDependencyManagement().getDependencies(), 127 "dependencyManagement.dependencies.dependency", request ); 128 } 129 130 validateRawRepositories( problems, model.getRepositories(), "repositories.repository", request ); 131 132 validateRawRepositories( problems, model.getPluginRepositories(), "pluginRepositories.pluginRepository", 133 request ); 134 135 Build build = model.getBuild(); 136 if ( build != null ) 137 { 138 validate20RawPlugins( problems, build.getPlugins(), "build.plugins.plugin", request ); 139 140 PluginManagement mngt = build.getPluginManagement(); 141 if ( mngt != null ) 142 { 143 validate20RawPlugins( problems, mngt.getPlugins(), "build.pluginManagement.plugins.plugin", request ); 144 } 145 } 146 147 Set<String> profileIds = new HashSet<String>(); 148 149 for ( Profile profile : model.getProfiles() ) 150 { 151 String prefix = "profiles.profile[" + profile.getId() + "]"; 152 153 if ( !profileIds.add( profile.getId() ) ) 154 { 155 addViolation( problems, errOn30, Version.V20, "profiles.profile.id", null, 156 "must be unique but found duplicate profile with id " + profile.getId(), profile ); 157 } 158 159 validate30RawProfileActivation( problems, profile.getActivation(), profile.getId(), prefix + ".activation", request ); 160 161 validate20RawDependencies( problems, profile.getDependencies(), prefix + ".dependencies.dependency", 162 request ); 163 164 if ( profile.getDependencyManagement() != null ) 165 { 166 validate20RawDependencies( problems, profile.getDependencyManagement().getDependencies(), prefix 167 + ".dependencyManagement.dependencies.dependency", request ); 168 } 169 170 validateRawRepositories( problems, profile.getRepositories(), prefix + ".repositories.repository", 171 request ); 172 173 validateRawRepositories( problems, profile.getPluginRepositories(), prefix 174 + ".pluginRepositories.pluginRepository", request ); 175 176 BuildBase buildBase = profile.getBuild(); 177 if ( buildBase != null ) 178 { 179 validate20RawPlugins( problems, buildBase.getPlugins(), prefix + ".plugins.plugin", request ); 180 181 PluginManagement mngt = buildBase.getPluginManagement(); 182 if ( mngt != null ) 183 { 184 validate20RawPlugins( problems, mngt.getPlugins(), prefix + ".pluginManagement.plugins.plugin", 185 request ); 186 } 187 } 188 } 189 } 190 } 191 192 private void validate30RawProfileActivation( ModelProblemCollector problems, Activation activation, 193 String sourceHint, String prefix, ModelBuildingRequest request ) 194 { 195 if ( activation == null ) 196 { 197 return; 198 } 199 200 ActivationFile file = activation.getFile(); 201 202 if ( file != null ) 203 { 204 String path; 205 boolean missing; 206 207 if ( StringUtils.isNotEmpty( file.getExists() ) ) 208 { 209 path = file.getExists(); 210 missing = false; 211 } 212 else if ( StringUtils.isNotEmpty( file.getMissing() ) ) 213 { 214 path = file.getMissing(); 215 missing = true; 216 } 217 else 218 { 219 return; 220 } 221 222 if ( path.contains( "${project.basedir}" ) ) 223 { 224 addViolation( problems, 225 Severity.WARNING, 226 Version.V30, 227 prefix + ( missing ? ".file.missing" : ".file.exists" ), 228 null, 229 "Failed to interpolate file location " 230 + path 231 + " for profile " 232 + sourceHint 233 + ": ${project.basedir} expression not supported during profile activation, use ${basedir} instead", 234 file.getLocation( missing ? "missing" : "exists" ) ); 235 } 236 else if ( hasProjectExpression( path ) ) 237 { 238 addViolation( problems, 239 Severity.WARNING, 240 Version.V30, 241 prefix + ( missing ? ".file.missing" : ".file.exists" ), 242 null, 243 "Failed to interpolate file location " 244 + path 245 + " for profile " 246 + sourceHint 247 + ": ${project.*} expressions are not supported during profile activation", 248 file.getLocation( missing ? "missing" : "exists" ) ); 249 } 250 } 251 } 252 253 private void validate20RawPlugins( ModelProblemCollector problems, List<Plugin> plugins, String prefix, 254 ModelBuildingRequest request ) 255 { 256 Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 ); 257 258 Map<String, Plugin> index = new HashMap<String, Plugin>(); 259 260 for ( Plugin plugin : plugins ) 261 { 262 String key = plugin.getKey(); 263 264 Plugin existing = index.get( key ); 265 266 if ( existing != null ) 267 { 268 addViolation( problems, errOn31, Version.V20, prefix + ".(groupId:artifactId)", null, 269 "must be unique but found duplicate declaration of plugin " + key, plugin ); 270 } 271 else 272 { 273 index.put( key, plugin ); 274 } 275 276 Set<String> executionIds = new HashSet<String>(); 277 278 for ( PluginExecution exec : plugin.getExecutions() ) 279 { 280 if ( !executionIds.add( exec.getId() ) ) 281 { 282 addViolation( problems, Severity.ERROR, Version.V20, prefix + "[" + plugin.getKey() 283 + "].executions.execution.id", null, "must be unique but found duplicate execution with id " 284 + exec.getId(), exec ); 285 } 286 } 287 } 288 } 289 290 public void validateEffectiveModel( Model model, ModelBuildingRequest request, ModelProblemCollector problems ) 291 { 292 validateStringNotEmpty( "modelVersion", problems, Severity.ERROR, Version.BASE, model.getModelVersion(), model ); 293 294 validateId( "groupId", problems, model.getGroupId(), model ); 295 296 validateId( "artifactId", problems, model.getArtifactId(), model ); 297 298 validateStringNotEmpty( "packaging", problems, Severity.ERROR, Version.BASE, model.getPackaging(), model ); 299 300 if ( !model.getModules().isEmpty() ) 301 { 302 if ( !"pom".equals( model.getPackaging() ) ) 303 { 304 addViolation( problems, Severity.ERROR, Version.BASE, "packaging", null, 305 "with value '" + model.getPackaging() + "' is invalid. Aggregator projects " 306 + "require 'pom' as packaging.", model ); 307 } 308 309 for ( int i = 0, n = model.getModules().size(); i < n; i++ ) 310 { 311 String module = model.getModules().get( i ); 312 if ( StringUtils.isBlank( module ) ) 313 { 314 addViolation( problems, Severity.WARNING, Version.BASE, "modules.module[" + i + "]", null, 315 "has been specified without a path to the project directory.", 316 model.getLocation( "modules" ) ); 317 } 318 } 319 } 320 321 validateStringNotEmpty( "version", problems, Severity.ERROR, Version.BASE, model.getVersion(), model ); 322 323 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 324 325 validateEffectiveDependencies( problems, model.getDependencies(), false, request ); 326 327 DependencyManagement mgmt = model.getDependencyManagement(); 328 if ( mgmt != null ) 329 { 330 validateEffectiveDependencies( problems, mgmt.getDependencies(), true, request ); 331 } 332 333 if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) 334 { 335 Set<String> modules = new HashSet<String>(); 336 for ( int i = 0, n = model.getModules().size(); i < n; i++ ) 337 { 338 String module = model.getModules().get( i ); 339 if ( !modules.add( module ) ) 340 { 341 addViolation( problems, Severity.ERROR, Version.V20, "modules.module[" + i + "]", null, 342 "specifies duplicate child module " + module, model.getLocation( "modules" ) ); 343 } 344 } 345 346 Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 ); 347 348 validateBannedCharacters( "version", problems, errOn31, Version.V20, model.getVersion(), null, model, 349 ILLEGAL_VERSION_CHARS ); 350 validate20ProperSnapshotVersion( "version", problems, errOn31, Version.V20, model.getVersion(), null, model ); 351 352 Build build = model.getBuild(); 353 if ( build != null ) 354 { 355 for ( Plugin p : build.getPlugins() ) 356 { 357 validateStringNotEmpty( "build.plugins.plugin.artifactId", problems, Severity.ERROR, Version.V20, 358 p.getArtifactId(), p ); 359 360 validateStringNotEmpty( "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(), 361 p ); 362 363 validate20PluginVersion( "build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p, 364 request ); 365 366 validateBoolean( "build.plugins.plugin.inherited", problems, errOn30, Version.V20, p.getInherited(), p.getKey(), 367 p ); 368 369 validateBoolean( "build.plugins.plugin.extensions", problems, errOn30, Version.V20, p.getExtensions(), 370 p.getKey(), p ); 371 372 validate20EffectivePluginDependencies( problems, p, request ); 373 } 374 375 validate20RawResources( problems, build.getResources(), "build.resources.resource", request ); 376 377 validate20RawResources( problems, build.getTestResources(), "build.testResources.testResource", request ); 378 } 379 380 Reporting reporting = model.getReporting(); 381 if ( reporting != null ) 382 { 383 for ( ReportPlugin p : reporting.getPlugins() ) 384 { 385 validateStringNotEmpty( "reporting.plugins.plugin.artifactId", problems, Severity.ERROR, Version.V20, 386 p.getArtifactId(), p ); 387 388 validateStringNotEmpty( "reporting.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, 389 p.getGroupId(), p ); 390 } 391 } 392 393 for ( Repository repository : model.getRepositories() ) 394 { 395 validate20EffectiveRepository( problems, repository, "repositories.repository", request ); 396 } 397 398 for ( Repository repository : model.getPluginRepositories() ) 399 { 400 validate20EffectiveRepository( problems, repository, "pluginRepositories.pluginRepository", request ); 401 } 402 403 DistributionManagement distMgmt = model.getDistributionManagement(); 404 if ( distMgmt != null ) 405 { 406 if ( distMgmt.getStatus() != null ) 407 { 408 addViolation( problems, Severity.ERROR, Version.V20, "distributionManagement.status", null, 409 "must not be specified.", distMgmt ); 410 } 411 412 validate20EffectiveRepository( problems, distMgmt.getRepository(), "distributionManagement.repository", request ); 413 validate20EffectiveRepository( problems, distMgmt.getSnapshotRepository(), 414 "distributionManagement.snapshotRepository", request ); 415 } 416 } 417 } 418 419 private void validate20RawDependencies( ModelProblemCollector problems, List<Dependency> dependencies, String prefix, 420 ModelBuildingRequest request ) 421 { 422 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 423 Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 ); 424 425 Map<String, Dependency> index = new HashMap<String, Dependency>(); 426 427 for ( Dependency dependency : dependencies ) 428 { 429 String key = dependency.getManagementKey(); 430 431 if ( "import".equals( dependency.getScope() ) ) 432 { 433 if ( !"pom".equals( dependency.getType() ) ) 434 { 435 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".type", key, 436 "must be 'pom' to import the managed dependencies.", dependency ); 437 } 438 else if ( StringUtils.isNotEmpty( dependency.getClassifier() ) ) 439 { 440 addViolation( problems, errOn30, Version.V20, prefix + ".classifier", key, 441 "must be empty, imported POM cannot have a classifier.", dependency ); 442 } 443 } 444 else if ( "system".equals( dependency.getScope() ) ) 445 { 446 String sysPath = dependency.getSystemPath(); 447 if ( StringUtils.isNotEmpty( sysPath ) ) 448 { 449 if ( !hasExpression( sysPath ) ) 450 { 451 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key, 452 "should use a variable instead of a hard-coded path " + sysPath, dependency ); 453 } 454 else if ( sysPath.contains( "${basedir}" ) || sysPath.contains( "${project.basedir}" ) ) 455 { 456 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key, 457 "should not point at files within the project directory, " + sysPath 458 + " will be unresolvable by dependent projects", dependency ); 459 } 460 } 461 } 462 463 Dependency existing = index.get( key ); 464 465 if ( existing != null ) 466 { 467 String msg; 468 if ( equals( existing.getVersion(), dependency.getVersion() ) ) 469 { 470 msg = 471 "duplicate declaration of version " 472 + StringUtils.defaultString( dependency.getVersion(), "(?)" ); 473 } 474 else 475 { 476 msg = 477 "version " + StringUtils.defaultString( existing.getVersion(), "(?)" ) + " vs " 478 + StringUtils.defaultString( dependency.getVersion(), "(?)" ); 479 } 480 481 addViolation( problems, errOn31, Version.V20, prefix + ".(groupId:artifactId:type:classifier)", null, 482 "must be unique: " + key + " -> " + msg, dependency ); 483 } 484 else 485 { 486 index.put( key, dependency ); 487 } 488 } 489 } 490 491 private void validateEffectiveDependencies( ModelProblemCollector problems, List<Dependency> dependencies, 492 boolean management, ModelBuildingRequest request ) 493 { 494 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 495 496 String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency."; 497 498 for ( Dependency d : dependencies ) 499 { 500 validateEffectiveDependency( problems, d, management, prefix, request ); 501 502 if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) 503 { 504 validateBoolean( prefix + "optional", problems, errOn30, Version.V20, d.getOptional(), d.getManagementKey(), d ); 505 506 if ( !management ) 507 { 508 validateVersion( prefix + "version", problems, errOn30, Version.V20, d.getVersion(), d.getManagementKey(), d ); 509 510 /* 511 * TODO: Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. 512 * In order to don't break backward-compat with those, only warn but don't error out. 513 */ 514 validateEnum( prefix + "scope", problems, Severity.WARNING, Version.V20, d.getScope(), d.getManagementKey(), d, 515 "provided", "compile", "runtime", "test", "system" ); 516 } 517 } 518 } 519 } 520 521 private void validate20EffectivePluginDependencies( ModelProblemCollector problems, Plugin plugin, 522 ModelBuildingRequest request ) 523 { 524 List<Dependency> dependencies = plugin.getDependencies(); 525 526 if ( !dependencies.isEmpty() ) 527 { 528 String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency."; 529 530 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 531 532 for ( Dependency d : dependencies ) 533 { 534 validateEffectiveDependency( problems, d, false, prefix, request ); 535 536 validateVersion( prefix + "version", problems, errOn30, Version.BASE, d.getVersion(), d.getManagementKey(), d ); 537 538 validateEnum( prefix + "scope", problems, errOn30, Version.BASE, d.getScope(), d.getManagementKey(), d, "compile", 539 "runtime", "system" ); 540 } 541 } 542 } 543 544 private void validateEffectiveDependency( ModelProblemCollector problems, Dependency d, boolean management, 545 String prefix, ModelBuildingRequest request ) 546 { 547 validateId( prefix + "artifactId", problems, Severity.ERROR, Version.BASE, d.getArtifactId(), d.getManagementKey(), d ); 548 549 validateId( prefix + "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(), d ); 550 551 if ( !management ) 552 { 553 validateStringNotEmpty( prefix + "type", problems, Severity.ERROR, Version.BASE, d.getType(), d.getManagementKey(), d ); 554 555 validateStringNotEmpty( prefix + "version", problems, Severity.ERROR, Version.BASE, d.getVersion(), d.getManagementKey(), 556 d ); 557 } 558 559 if ( "system".equals( d.getScope() ) ) 560 { 561 String systemPath = d.getSystemPath(); 562 563 if ( StringUtils.isEmpty( systemPath ) ) 564 { 565 addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(), "is missing.", 566 d ); 567 } 568 else 569 { 570 File sysFile = new File( systemPath ); 571 if ( !sysFile.isAbsolute() ) 572 { 573 addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(), 574 "must specify an absolute path but is " + systemPath, d ); 575 } 576 else if ( !sysFile.isFile() ) 577 { 578 String msg = "refers to a non-existing file " + sysFile.getAbsolutePath(); 579 systemPath = systemPath.replace( '/', File.separatorChar ).replace( '\\', File.separatorChar ); 580 String jdkHome = 581 request.getSystemProperties().getProperty( "java.home", "" ) + File.separator + ".."; 582 if ( systemPath.startsWith( jdkHome ) ) 583 { 584 msg += ". Please verify that you run Maven using a JDK and not just a JRE."; 585 } 586 addViolation( problems, Severity.WARNING, Version.BASE, prefix + "systemPath", d.getManagementKey(), msg, d ); 587 } 588 } 589 } 590 else if ( StringUtils.isNotEmpty( d.getSystemPath() ) ) 591 { 592 addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(), "must be omitted." 593 + " This field may only be specified for a dependency with system scope.", d ); 594 } 595 596 if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 ) 597 { 598 for ( Exclusion exclusion : d.getExclusions() ) 599 { 600 if ( request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ) 601 { 602 validateId( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING, Version.V20, 603 exclusion.getGroupId(), d.getManagementKey(), exclusion ); 604 605 validateId( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING, Version.V20, 606 exclusion.getArtifactId(), d.getManagementKey(), exclusion ); 607 } 608 else 609 { 610 validateIdWithWildcards( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING, Version.V30, 611 exclusion.getGroupId(), d.getManagementKey(), exclusion ); 612 613 validateIdWithWildcards( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING, Version.V30, 614 exclusion.getArtifactId(), d.getManagementKey(), exclusion ); 615 } 616 } 617 } 618 } 619 620 private void validateRawRepositories( ModelProblemCollector problems, List<Repository> repositories, String prefix, 621 ModelBuildingRequest request ) 622 { 623 Map<String, Repository> index = new HashMap<String, Repository>(); 624 625 for ( Repository repository : repositories ) 626 { 627 validateStringNotEmpty( prefix + ".id", problems, Severity.ERROR, Version.V20, repository.getId(), repository ); 628 629 validateStringNotEmpty( prefix + "[" + repository.getId() + "].url", problems, Severity.ERROR, Version.V20, 630 repository.getUrl(), repository ); 631 632 String key = repository.getId(); 633 634 Repository existing = index.get( key ); 635 636 if ( existing != null ) 637 { 638 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 639 640 addViolation( problems, errOn30, Version.V20, prefix + ".id", null, "must be unique: " + repository.getId() + " -> " 641 + existing.getUrl() + " vs " + repository.getUrl(), repository ); 642 } 643 else 644 { 645 index.put( key, repository ); 646 } 647 } 648 } 649 650 private void validate20EffectiveRepository( ModelProblemCollector problems, Repository repository, String prefix, 651 ModelBuildingRequest request ) 652 { 653 if ( repository != null ) 654 { 655 Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 ); 656 657 validateBannedCharacters( prefix + ".id", problems, errOn31, Version.V20, repository.getId(), null, repository, 658 ILLEGAL_REPO_ID_CHARS ); 659 660 if ( "local".equals( repository.getId() ) ) 661 { 662 addViolation( problems, errOn31, Version.V20, prefix + ".id", null, "must not be 'local'" 663 + ", this identifier is reserved for the local repository" 664 + ", using it for other repositories will corrupt your repository metadata.", repository ); 665 } 666 667 if ( "legacy".equals( repository.getLayout() ) ) 668 { 669 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".layout", repository.getId(), 670 "uses the unsupported value 'legacy', artifact resolution might fail.", repository ); 671 } 672 } 673 } 674 675 private void validate20RawResources( ModelProblemCollector problems, List<Resource> resources, String prefix, 676 ModelBuildingRequest request ) 677 { 678 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 679 680 for ( Resource resource : resources ) 681 { 682 validateStringNotEmpty( prefix + ".directory", problems, Severity.ERROR, Version.V20, resource.getDirectory(), 683 resource ); 684 685 validateBoolean( prefix + ".filtering", problems, errOn30, Version.V20, resource.getFiltering(), 686 resource.getDirectory(), resource ); 687 } 688 } 689 690 // ---------------------------------------------------------------------- 691 // Field validation 692 // ---------------------------------------------------------------------- 693 694 private boolean validateId( String fieldName, ModelProblemCollector problems, String id, 695 InputLocationTracker tracker ) 696 { 697 return validateId( fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker ); 698 } 699 700 private boolean validateId( String fieldName, ModelProblemCollector problems, Severity severity, Version version, String id, 701 String sourceHint, InputLocationTracker tracker ) 702 { 703 if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) ) 704 { 705 return false; 706 } 707 else 708 { 709 boolean match = ID_REGEX.matcher( id ).matches(); 710 if ( !match ) 711 { 712 addViolation( problems, severity, version, fieldName, sourceHint, "with value '" + id 713 + "' does not match a valid id pattern.", tracker ); 714 } 715 return match; 716 } 717 } 718 719 private boolean validateIdWithWildcards( String fieldName, ModelProblemCollector problems, Severity severity, Version version, String id, 720 String sourceHint, InputLocationTracker tracker ) 721 { 722 if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) ) 723 { 724 return false; 725 } 726 else 727 { 728 boolean match = ID_WITH_WILDCARDS_REGEX.matcher( id ).matches(); 729 if ( !match ) 730 { 731 addViolation( problems, severity, version, fieldName, sourceHint, "with value '" + id 732 + "' does not match a valid id pattern.", tracker ); 733 } 734 return match; 735 } 736 } 737 738 739 private boolean validateStringNoExpression( String fieldName, ModelProblemCollector problems, Severity severity, Version version, 740 String string, InputLocationTracker tracker ) 741 { 742 if ( !hasExpression( string ) ) 743 { 744 return true; 745 } 746 747 addViolation( problems, severity, version, fieldName, null, "contains an expression but should be a constant.", 748 tracker ); 749 750 return false; 751 } 752 753 private boolean validateVersionNoExpression( String fieldName, ModelProblemCollector problems, Severity severity, 754 Version version, String string, InputLocationTracker tracker ) 755 { 756 757 if ( !hasExpression( string ) ) 758 { 759 return true; 760 } 761 762 // 763 // Acceptable versions for continuous delivery 764 // 765 // changelist 766 // revision 767 // sha1 768 // 769 if ( string.trim().contains( "${changelist}" ) || string.trim().contains( "${revision}" ) 770 || string.trim().contains( "${sha1}" ) ) 771 { 772 return true; 773 } 774 775 addViolation( problems, severity, version, fieldName, null, "contains an expression but should be a constant.", 776 tracker ); 777 778 return false; 779 } 780 781 private boolean hasExpression( String value ) 782 { 783 return value != null && value.contains( "${" ); 784 } 785 786 private boolean hasProjectExpression( String value ) 787 { 788 return value != null && value.contains( "${project." ); 789 } 790 791 private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity, Version version, 792 String string, InputLocationTracker tracker ) 793 { 794 return validateStringNotEmpty( fieldName, problems, severity, version, string, null, tracker ); 795 } 796 797 /** 798 * Asserts: 799 * <p/> 800 * <ul> 801 * <li><code>string != null</code> 802 * <li><code>string.length > 0</code> 803 * </ul> 804 */ 805 private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity, Version version, 806 String string, String sourceHint, InputLocationTracker tracker ) 807 { 808 if ( !validateNotNull( fieldName, problems, severity, version, string, sourceHint, tracker ) ) 809 { 810 return false; 811 } 812 813 if ( string.length() > 0 ) 814 { 815 return true; 816 } 817 818 addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker ); 819 820 return false; 821 } 822 823 /** 824 * Asserts: 825 * <p/> 826 * <ul> 827 * <li><code>string != null</code> 828 * </ul> 829 */ 830 private boolean validateNotNull( String fieldName, ModelProblemCollector problems, Severity severity, Version version, 831 Object object, String sourceHint, InputLocationTracker tracker ) 832 { 833 if ( object != null ) 834 { 835 return true; 836 } 837 838 addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker ); 839 840 return false; 841 } 842 843 private boolean validateBoolean( String fieldName, ModelProblemCollector problems, Severity severity, 844 Version version, String string, String sourceHint, InputLocationTracker tracker ) 845 { 846 if ( string == null || string.length() <= 0 ) 847 { 848 return true; 849 } 850 851 if ( "true".equalsIgnoreCase( string ) || "false".equalsIgnoreCase( string ) ) 852 { 853 return true; 854 } 855 856 addViolation( problems, severity, version, fieldName, sourceHint, "must be 'true' or 'false' but is '" + string 857 + "'.", tracker ); 858 859 return false; 860 } 861 862 private boolean validateEnum( String fieldName, ModelProblemCollector problems, Severity severity, Version version, 863 String string, String sourceHint, InputLocationTracker tracker, String... validValues ) 864 { 865 if ( string == null || string.length() <= 0 ) 866 { 867 return true; 868 } 869 870 List<String> values = Arrays.asList( validValues ); 871 872 if ( values.contains( string ) ) 873 { 874 return true; 875 } 876 877 addViolation( problems, severity, version, fieldName, sourceHint, "must be one of " + values + " but is '" + string 878 + "'.", tracker ); 879 880 return false; 881 } 882 883 private boolean validateBannedCharacters( String fieldName, ModelProblemCollector problems, Severity severity, 884 Version version, String string, String sourceHint, 885 InputLocationTracker tracker, String banned ) 886 { 887 if ( string != null ) 888 { 889 for ( int i = string.length() - 1; i >= 0; i-- ) 890 { 891 if ( banned.indexOf( string.charAt( i ) ) >= 0 ) 892 { 893 addViolation( problems, severity, version, fieldName, sourceHint, 894 "must not contain any of these characters " + banned + " but found " 895 + string.charAt( i ), tracker ); 896 return false; 897 } 898 } 899 } 900 901 return true; 902 } 903 904 private boolean validateVersion( String fieldName, ModelProblemCollector problems, Severity severity, 905 Version version, String string, String sourceHint, InputLocationTracker tracker ) 906 { 907 if ( string == null || string.length() <= 0 ) 908 { 909 return true; 910 } 911 912 if ( hasExpression( string ) ) 913 { 914 addViolation( problems, severity, version, fieldName, sourceHint, 915 "must be a valid version but is '" + string + "'.", tracker ); 916 return false; 917 } 918 919 return validateBannedCharacters( fieldName, problems, severity, version, string, sourceHint, tracker, 920 ILLEGAL_VERSION_CHARS ); 921 922 } 923 924 private boolean validate20ProperSnapshotVersion( String fieldName, ModelProblemCollector problems, 925 Severity severity, Version version, String string, 926 String sourceHint, InputLocationTracker tracker ) 927 { 928 if ( string == null || string.length() <= 0 ) 929 { 930 return true; 931 } 932 933 if ( string.endsWith( "SNAPSHOT" ) && !string.endsWith( "-SNAPSHOT" ) ) 934 { 935 addViolation( problems, severity, version, fieldName, sourceHint, 936 "uses an unsupported snapshot version format" + ", should be '*-SNAPSHOT' instead.", tracker ); 937 return false; 938 } 939 940 return true; 941 } 942 943 private boolean validate20PluginVersion( String fieldName, ModelProblemCollector problems, String string, 944 String sourceHint, InputLocationTracker tracker, 945 ModelBuildingRequest request ) 946 { 947 if ( string == null ) 948 { 949 // NOTE: The check for missing plugin versions is handled directly by the model builder 950 return true; 951 } 952 953 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 ); 954 955 if ( !validateVersion( fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker ) ) 956 { 957 return false; 958 } 959 960 if ( string.length() <= 0 || "RELEASE".equals( string ) || "LATEST".equals( string ) ) 961 { 962 addViolation( problems, errOn30, Version.V20, fieldName, sourceHint, "must be a valid version but is '" 963 + string + "'.", tracker ); 964 return false; 965 } 966 967 return true; 968 } 969 970 private static void addViolation( ModelProblemCollector problems, Severity severity, Version version, 971 String fieldName, String sourceHint, String message, InputLocationTracker tracker ) 972 { 973 StringBuilder buffer = new StringBuilder( 256 ); 974 buffer.append( '\'' ).append( fieldName ).append( '\'' ); 975 976 if ( sourceHint != null ) 977 { 978 buffer.append( " for " ).append( sourceHint ); 979 } 980 981 buffer.append( ' ' ).append( message ); 982 983 problems.add( new ModelProblemCollectorRequest( severity, version ) 984 .setMessage( buffer.toString() ).setLocation( getLocation( fieldName, tracker ) ) ); 985 } 986 987 private static InputLocation getLocation( String fieldName, InputLocationTracker tracker ) 988 { 989 InputLocation location = null; 990 991 if ( tracker != null ) 992 { 993 if ( fieldName != null ) 994 { 995 Object key = fieldName; 996 997 int idx = fieldName.lastIndexOf( '.' ); 998 if ( idx >= 0 ) 999 { 1000 fieldName = fieldName.substring( idx + 1 ); 1001 key = fieldName; 1002 } 1003 1004 if ( fieldName.endsWith( "]" ) ) 1005 { 1006 key = fieldName.substring( fieldName.lastIndexOf( '[' ) + 1, fieldName.length() - 1 ); 1007 try 1008 { 1009 key = Integer.valueOf( key.toString() ); 1010 } 1011 catch ( NumberFormatException e ) 1012 { 1013 // use key as is 1014 } 1015 } 1016 1017 location = tracker.getLocation( key ); 1018 } 1019 1020 if ( location == null ) 1021 { 1022 location = tracker.getLocation( "" ); 1023 } 1024 } 1025 1026 return location; 1027 } 1028 1029 private static boolean equals( String s1, String s2 ) 1030 { 1031 return StringUtils.clean( s1 ).equals( StringUtils.clean( s2 ) ); 1032 } 1033 1034 private static Severity getSeverity( ModelBuildingRequest request, int errorThreshold ) 1035 { 1036 return getSeverity( request.getValidationLevel(), errorThreshold ); 1037 } 1038 1039 private static Severity getSeverity( int validationLevel, int errorThreshold ) 1040 { 1041 if ( validationLevel < errorThreshold ) 1042 { 1043 return Severity.WARNING; 1044 } 1045 else 1046 { 1047 return Severity.ERROR; 1048 } 1049 } 1050 1051}