View Javadoc
1   package org.apache.maven.model.validation;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.util.Arrays;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import org.apache.maven.model.Activation;
33  import org.apache.maven.model.ActivationFile;
34  import org.apache.maven.model.Build;
35  import org.apache.maven.model.BuildBase;
36  import org.apache.maven.model.Dependency;
37  import org.apache.maven.model.DependencyManagement;
38  import org.apache.maven.model.DistributionManagement;
39  import org.apache.maven.model.Exclusion;
40  import org.apache.maven.model.InputLocation;
41  import org.apache.maven.model.InputLocationTracker;
42  import org.apache.maven.model.Model;
43  import org.apache.maven.model.Parent;
44  import org.apache.maven.model.Plugin;
45  import org.apache.maven.model.PluginExecution;
46  import org.apache.maven.model.PluginManagement;
47  import org.apache.maven.model.Profile;
48  import org.apache.maven.model.ReportPlugin;
49  import org.apache.maven.model.Reporting;
50  import org.apache.maven.model.Repository;
51  import org.apache.maven.model.Resource;
52  import org.apache.maven.model.building.ModelBuildingRequest;
53  import org.apache.maven.model.building.ModelProblem.Severity;
54  import org.apache.maven.model.building.ModelProblem.Version;
55  import org.apache.maven.model.building.ModelProblemCollector;
56  import org.apache.maven.model.building.ModelProblemCollectorRequest;
57  import org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator;
58  import org.codehaus.plexus.component.annotations.Component;
59  import org.codehaus.plexus.util.StringUtils;
60  
61  /**
62   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
63   */
64  @Component( role = ModelValidator.class )
65  public class DefaultModelValidator
66      implements ModelValidator
67  {
68  
69      private static final Pattern CI_FRIENDLY_EXPRESSION = Pattern.compile( "\\$\\{(.+?)\\}" );
70  
71      private static final List<String> CI_FRIENDLY_POSSIBLE_PROPERTY_NAMES =
72          Arrays.asList( AbstractStringBasedModelInterpolator.REVISION_PROPERTY,
73                         AbstractStringBasedModelInterpolator.CHANGELIST_PROPERTY,
74                         AbstractStringBasedModelInterpolator.SHA1_PROPERTY );
75  
76      private static final Pattern ID_REGEX = Pattern.compile( "[A-Za-z0-9_\\-.]+" );
77  
78      private static final Pattern ID_WITH_WILDCARDS_REGEX = Pattern.compile( "[A-Za-z0-9_\\-.?*]+" );
79  
80      private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";
81  
82      private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;
83  
84      private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;
85  
86      @Override
87      public void validateRawModel( Model m, ModelBuildingRequest request, ModelProblemCollector problems )
88      {
89          Parent parent = m.getParent();
90          if ( parent != null )
91          {
92              validateStringNotEmpty( "parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(),
93                                      parent );
94  
95              validateStringNotEmpty( "parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(),
96                                      parent );
97  
98              validateStringNotEmpty( "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(),
99                                      parent );
100 
101             if ( equals( parent.getGroupId(), m.getGroupId() ) && equals( parent.getArtifactId(), m.getArtifactId() ) )
102             {
103                 addViolation( problems, Severity.FATAL, Version.BASE, "parent.artifactId", null,
104                               "must be changed"
105                                   + ", the parent element cannot have the same groupId:artifactId as the project.",
106                               parent );
107             }
108 
109             if ( equals( "LATEST", parent.getVersion() ) || equals( "RELEASE", parent.getVersion() ) )
110             {
111                 addViolation( problems, Severity.WARNING, Version.BASE, "parent.version", null,
112                               "is either LATEST or RELEASE (both of them are being deprecated)", parent );
113             }
114 
115         }
116 
117         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
118         {
119             Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
120 
121             // [MNG-6074] Maven should produce an error if no model version has been set in a POM file used to build an
122             // effective model.
123             //
124             // As of 3.4, the model version is mandatory even in raw models. The XML element still is optional in the
125             // XML schema and this will not change anytime soon. We do not want to build effective models based on
126             // models without a version starting with 3.4.
127             validateStringNotEmpty( "modelVersion", problems, Severity.ERROR, Version.V20, m.getModelVersion(), m );
128 
129             validateEnum( "modelVersion", problems, Severity.ERROR, Version.V20, m.getModelVersion(), null, m,
130                           "4.0.0" );
131 
132             validateStringNoExpression( "groupId", problems, Severity.WARNING, Version.V20, m.getGroupId(), m );
133             if ( parent == null )
134             {
135                 validateStringNotEmpty( "groupId", problems, Severity.FATAL, Version.V20, m.getGroupId(), m );
136             }
137 
138             validateStringNoExpression( "artifactId", problems, Severity.WARNING, Version.V20, m.getArtifactId(), m );
139             validateStringNotEmpty( "artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m );
140 
141             validateVersionNoExpression( "version", problems, Severity.WARNING, Version.V20, m.getVersion(), m );
142             if ( parent == null )
143             {
144                 validateStringNotEmpty( "version", problems, Severity.FATAL, Version.V20, m.getVersion(), m );
145             }
146 
147             validate20RawDependencies( problems, m.getDependencies(), "dependencies.dependency", request );
148 
149             validate20RawDependenciesSelfReferencing( problems, m, m.getDependencies(), "dependencies.dependency",
150                                                       request );
151 
152             if ( m.getDependencyManagement() != null )
153             {
154                 validate20RawDependencies( problems, m.getDependencyManagement().getDependencies(),
155                                            "dependencyManagement.dependencies.dependency", request );
156             }
157 
158             validateRawRepositories( problems, m.getRepositories(), "repositories.repository", request );
159 
160             validateRawRepositories( problems, m.getPluginRepositories(), "pluginRepositories.pluginRepository",
161                                      request );
162 
163             Build build = m.getBuild();
164             if ( build != null )
165             {
166                 validate20RawPlugins( problems, build.getPlugins(), "build.plugins.plugin", request );
167 
168                 PluginManagement mgmt = build.getPluginManagement();
169                 if ( mgmt != null )
170                 {
171                     validate20RawPlugins( problems, mgmt.getPlugins(), "build.pluginManagement.plugins.plugin",
172                                           request );
173                 }
174             }
175 
176             Set<String> profileIds = new HashSet<>();
177 
178             for ( Profile profile : m.getProfiles() )
179             {
180                 String prefix = "profiles.profile[" + profile.getId() + "]";
181 
182                 if ( !profileIds.add( profile.getId() ) )
183                 {
184                     addViolation( problems, errOn30, Version.V20, "profiles.profile.id", null,
185                                   "must be unique but found duplicate profile with id " + profile.getId(), profile );
186                 }
187 
188                 validate30RawProfileActivation( problems, profile.getActivation(), profile.getId(),
189                                                 prefix + ".activation", request );
190 
191                 validate20RawDependencies( problems, profile.getDependencies(), prefix + ".dependencies.dependency",
192                                            request );
193 
194                 if ( profile.getDependencyManagement() != null )
195                 {
196                     validate20RawDependencies( problems, profile.getDependencyManagement().getDependencies(),
197                                                prefix + ".dependencyManagement.dependencies.dependency", request );
198                 }
199 
200                 validateRawRepositories( problems, profile.getRepositories(), prefix + ".repositories.repository",
201                                          request );
202 
203                 validateRawRepositories( problems, profile.getPluginRepositories(),
204                                          prefix + ".pluginRepositories.pluginRepository", request );
205 
206                 BuildBase buildBase = profile.getBuild();
207                 if ( buildBase != null )
208                 {
209                     validate20RawPlugins( problems, buildBase.getPlugins(), prefix + ".plugins.plugin", request );
210 
211                     PluginManagement mgmt = buildBase.getPluginManagement();
212                     if ( mgmt != null )
213                     {
214                         validate20RawPlugins( problems, mgmt.getPlugins(), prefix + ".pluginManagement.plugins.plugin",
215                                               request );
216                     }
217                 }
218             }
219         }
220     }
221 
222     private void validate30RawProfileActivation( ModelProblemCollector problems, Activation activation,
223                                                  String sourceHint, String prefix, ModelBuildingRequest request )
224     {
225         if ( activation == null )
226         {
227             return;
228         }
229 
230         ActivationFile file = activation.getFile();
231 
232         if ( file != null )
233         {
234             String path;
235             boolean missing;
236 
237             if ( StringUtils.isNotEmpty( file.getExists() ) )
238             {
239                 path = file.getExists();
240                 missing = false;
241             }
242             else if ( StringUtils.isNotEmpty( file.getMissing() ) )
243             {
244                 path = file.getMissing();
245                 missing = true;
246             }
247             else
248             {
249                 return;
250             }
251 
252             if ( path.contains( "${project.basedir}" ) )
253             {
254                 addViolation( problems, Severity.WARNING, Version.V30,
255                               prefix + ( missing ? ".file.missing" : ".file.exists" ), null,
256                               "Failed to interpolate file location " + path + " for profile " + sourceHint
257                                   + ": ${project.basedir} expression not supported during profile activation, "
258                                   + "use ${basedir} instead",
259                               file.getLocation( missing ? "missing" : "exists" ) );
260             }
261             else if ( hasProjectExpression( path ) )
262             {
263                 addViolation( problems, Severity.WARNING, Version.V30,
264                               prefix + ( missing ? ".file.missing" : ".file.exists" ), null,
265                               "Failed to interpolate file location " + path + " for profile " + sourceHint
266                                   + ": ${project.*} expressions are not supported during profile activation",
267                               file.getLocation( missing ? "missing" : "exists" ) );
268             }
269         }
270     }
271 
272     private void validate20RawPlugins( ModelProblemCollector problems, List<Plugin> plugins, String prefix,
273                                        ModelBuildingRequest request )
274     {
275         Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
276 
277         Map<String, Plugin> index = new HashMap<>();
278 
279         for ( Plugin plugin : plugins )
280         {
281             if ( plugin.getGroupId() == null
282                 || ( plugin.getGroupId() != null && plugin.getGroupId().trim().isEmpty() ) )
283             {
284                 addViolation( problems, Severity.FATAL, Version.V20, prefix + ".(groupId:artifactId)", null,
285                               "groupId of a plugin must be defined. ", plugin );
286             }
287 
288             if ( plugin.getArtifactId() == null
289                 || ( plugin.getArtifactId() != null && plugin.getArtifactId().trim().isEmpty() ) )
290             {
291                 addViolation( problems, Severity.FATAL, Version.V20, prefix + ".(groupId:artifactId)", null,
292                               "artifactId of a plugin must be defined. ", plugin );
293             }
294 
295             // This will catch cases like <version></version> or <version/>
296             if ( plugin.getVersion() != null && plugin.getVersion().trim().isEmpty() )
297             {
298                 addViolation( problems, Severity.FATAL, Version.V20, prefix + ".(groupId:artifactId)", null,
299                               "version of a plugin must be defined. ", plugin );
300             }
301 
302             String key = plugin.getKey();
303 
304             Plugin existing = index.get( key );
305 
306             if ( existing != null )
307             {
308                 addViolation( problems, errOn31, Version.V20, prefix + ".(groupId:artifactId)", null,
309                               "must be unique but found duplicate declaration of plugin " + key, plugin );
310             }
311             else
312             {
313                 index.put( key, plugin );
314             }
315 
316             Set<String> executionIds = new HashSet<>();
317 
318             for ( PluginExecution exec : plugin.getExecutions() )
319             {
320                 if ( !executionIds.add( exec.getId() ) )
321                 {
322                     addViolation( problems, Severity.ERROR, Version.V20,
323                                   prefix + "[" + plugin.getKey() + "].executions.execution.id", null,
324                                   "must be unique but found duplicate execution with id " + exec.getId(), exec );
325                 }
326             }
327         }
328     }
329 
330     @Override
331     public void validateEffectiveModel( Model m, ModelBuildingRequest request, ModelProblemCollector problems )
332     {
333         validateStringNotEmpty( "modelVersion", problems, Severity.ERROR, Version.BASE, m.getModelVersion(), m );
334 
335         validateId( "groupId", problems, m.getGroupId(), m );
336 
337         validateId( "artifactId", problems, m.getArtifactId(), m );
338 
339         validateStringNotEmpty( "packaging", problems, Severity.ERROR, Version.BASE, m.getPackaging(), m );
340 
341         if ( !m.getModules().isEmpty() )
342         {
343             if ( !"pom".equals( m.getPackaging() ) )
344             {
345                 addViolation( problems, Severity.ERROR, Version.BASE, "packaging", null, "with value '"
346                     + m.getPackaging() + "' is invalid. Aggregator projects " + "require 'pom' as packaging.", m );
347             }
348 
349             for ( int i = 0, n = m.getModules().size(); i < n; i++ )
350             {
351                 String module = m.getModules().get( i );
352                 if ( StringUtils.isBlank( module ) )
353                 {
354                     addViolation( problems, Severity.ERROR, Version.BASE, "modules.module[" + i + "]", null,
355                                   "has been specified without a path to the project directory.",
356                                   m.getLocation( "modules" ) );
357                 }
358             }
359         }
360 
361         validateStringNotEmpty( "version", problems, Severity.ERROR, Version.BASE, m.getVersion(), m );
362 
363         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
364 
365         validateEffectiveDependencies( problems, m, m.getDependencies(), false, request );
366 
367         DependencyManagement mgmt = m.getDependencyManagement();
368         if ( mgmt != null )
369         {
370             validateEffectiveDependencies( problems, m, mgmt.getDependencies(), true, request );
371         }
372 
373         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
374         {
375             Set<String> modules = new HashSet<>();
376             for ( int i = 0, n = m.getModules().size(); i < n; i++ )
377             {
378                 String module = m.getModules().get( i );
379                 if ( !modules.add( module ) )
380                 {
381                     addViolation( problems, Severity.ERROR, Version.V20, "modules.module[" + i + "]", null,
382                                   "specifies duplicate child module " + module, m.getLocation( "modules" ) );
383                 }
384             }
385 
386             Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
387 
388             validateBannedCharacters( "version", problems, errOn31, Version.V20, m.getVersion(), null, m,
389                                       ILLEGAL_VERSION_CHARS );
390             validate20ProperSnapshotVersion( "version", problems, errOn31, Version.V20, m.getVersion(), null, m );
391 
392             Build build = m.getBuild();
393             if ( build != null )
394             {
395                 for ( Plugin p : build.getPlugins() )
396                 {
397                     validateStringNotEmpty( "build.plugins.plugin.artifactId", problems, Severity.ERROR, Version.V20,
398                                             p.getArtifactId(), p );
399 
400                     validateStringNotEmpty( "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20,
401                                             p.getGroupId(), p );
402 
403                     validate20PluginVersion( "build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p,
404                                              request );
405 
406                     validateBoolean( "build.plugins.plugin.inherited", problems, errOn30, Version.V20, p.getInherited(),
407                                      p.getKey(), p );
408 
409                     validateBoolean( "build.plugins.plugin.extensions", problems, errOn30, Version.V20,
410                                      p.getExtensions(), p.getKey(), p );
411 
412                     validate20EffectivePluginDependencies( problems, p, request );
413                 }
414 
415                 validate20RawResources( problems, build.getResources(), "build.resources.resource", request );
416 
417                 validate20RawResources( problems, build.getTestResources(), "build.testResources.testResource",
418                                         request );
419             }
420 
421             Reporting reporting = m.getReporting();
422             if ( reporting != null )
423             {
424                 for ( ReportPlugin p : reporting.getPlugins() )
425                 {
426                     validateStringNotEmpty( "reporting.plugins.plugin.artifactId", problems, Severity.ERROR,
427                                             Version.V20, p.getArtifactId(), p );
428 
429                     validateStringNotEmpty( "reporting.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20,
430                                             p.getGroupId(), p );
431                 }
432             }
433 
434             for ( Repository repository : m.getRepositories() )
435             {
436                 validate20EffectiveRepository( problems, repository, "repositories.repository", request );
437             }
438 
439             for ( Repository repository : m.getPluginRepositories() )
440             {
441                 validate20EffectiveRepository( problems, repository, "pluginRepositories.pluginRepository", request );
442             }
443 
444             DistributionManagement distMgmt = m.getDistributionManagement();
445             if ( distMgmt != null )
446             {
447                 if ( distMgmt.getStatus() != null )
448                 {
449                     addViolation( problems, Severity.ERROR, Version.V20, "distributionManagement.status", null,
450                                   "must not be specified.", distMgmt );
451                 }
452 
453                 validate20EffectiveRepository( problems, distMgmt.getRepository(), "distributionManagement.repository",
454                                                request );
455                 validate20EffectiveRepository( problems, distMgmt.getSnapshotRepository(),
456                                                "distributionManagement.snapshotRepository", request );
457             }
458         }
459     }
460 
461     private void validate20RawDependencies( ModelProblemCollector problems, List<Dependency> dependencies,
462                                             String prefix, ModelBuildingRequest request )
463     {
464         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
465         Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
466 
467         Map<String, Dependency> index = new HashMap<>();
468 
469         for ( Dependency dependency : dependencies )
470         {
471             String key = dependency.getManagementKey();
472 
473             if ( "import".equals( dependency.getScope() ) )
474             {
475                 if ( !"pom".equals( dependency.getType() ) )
476                 {
477                     addViolation( problems, Severity.WARNING, Version.V20, prefix + ".type", key,
478                                   "must be 'pom' to import the managed dependencies.", dependency );
479                 }
480                 else if ( StringUtils.isNotEmpty( dependency.getClassifier() ) )
481                 {
482                     addViolation( problems, errOn30, Version.V20, prefix + ".classifier", key,
483                                   "must be empty, imported POM cannot have a classifier.", dependency );
484                 }
485             }
486             else if ( "system".equals( dependency.getScope() ) )
487             {
488 
489                 if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 )
490                 {
491                     addViolation( problems, Severity.WARNING, Version.V31, prefix + ".scope", key,
492                                   "declares usage of deprecated 'system' scope ", dependency );
493                 }
494 
495                 String sysPath = dependency.getSystemPath();
496                 if ( StringUtils.isNotEmpty( sysPath ) )
497                 {
498                     if ( !hasExpression( sysPath ) )
499                     {
500                         addViolation( problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key,
501                                       "should use a variable instead of a hard-coded path " + sysPath, dependency );
502                     }
503                     else if ( sysPath.contains( "${basedir}" ) || sysPath.contains( "${project.basedir}" ) )
504                     {
505                         addViolation( problems, Severity.WARNING, Version.V20, prefix + ".systemPath", key,
506                                       "should not point at files within the project directory, " + sysPath
507                                           + " will be unresolvable by dependent projects",
508                                       dependency );
509                     }
510                 }
511             }
512 
513             if ( equals( "LATEST", dependency.getVersion() ) || equals( "RELEASE", dependency.getVersion() ) )
514             {
515                 addViolation( problems, Severity.WARNING, Version.BASE, prefix + ".version", key,
516                               "is either LATEST or RELEASE (both of them are being deprecated)", dependency );
517             }
518 
519             Dependency existing = index.get( key );
520 
521             if ( existing != null )
522             {
523                 String msg;
524                 if ( equals( existing.getVersion(), dependency.getVersion() ) )
525                 {
526                     msg = "duplicate declaration of version "
527                         + StringUtils.defaultString( dependency.getVersion(), "(?)" );
528                 }
529                 else
530                 {
531                     msg = "version " + StringUtils.defaultString( existing.getVersion(), "(?)" ) + " vs "
532                         + StringUtils.defaultString( dependency.getVersion(), "(?)" );
533                 }
534 
535                 addViolation( problems, errOn31, Version.V20, prefix + ".(groupId:artifactId:type:classifier)", null,
536                               "must be unique: " + key + " -> " + msg, dependency );
537             }
538             else
539             {
540                 index.put( key, dependency );
541             }
542         }
543     }
544 
545     private void validate20RawDependenciesSelfReferencing( ModelProblemCollector problems, Model m,
546                                                            List<Dependency> dependencies, String prefix,
547                                                            ModelBuildingRequest request )
548     {
549         // We only check for groupId/artifactId/version/classifier cause if there is another
550         // module with the same groupId/artifactId/version/classifier this will fail the build
551         // earlier like "Project '...' is duplicated in the reactor.
552         // So it is sufficient to check only groupId/artifactId/version/classifier and not the
553         // packaging type.
554         for ( Dependency dependency : dependencies )
555         {
556             String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
557                     + ( dependency.getClassifier() != null ? ":" + dependency.getClassifier() : ""  );
558             String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
559             if ( key.equals( mKey ) )
560             {
561                 // This means a module which is build has a dependency which has the same
562                 // groupId, artifactId, version and classifier coordinates. This is in consequence
563                 // a self reference or in other words a circular reference which can not being resolved.
564                 addViolation( problems, Severity.FATAL, Version.V31, prefix + " " + key, key, "is referencing itself.",
565                               dependency );
566 
567             }
568         }
569     }
570 
571     private void validateEffectiveDependencies( ModelProblemCollector problems, Model m, List<Dependency> dependencies,
572                                                 boolean management, ModelBuildingRequest request )
573     {
574         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
575 
576         String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";
577 
578         for ( Dependency d : dependencies )
579         {
580             validateEffectiveDependency( problems, d, management, prefix, request );
581 
582             if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
583             {
584                 validateBoolean( prefix + "optional", problems, errOn30, Version.V20, d.getOptional(),
585                                  d.getManagementKey(), d );
586 
587                 if ( !management )
588                 {
589                     validateVersion( prefix + "version", problems, errOn30, Version.V20, d.getVersion(),
590                                      d.getManagementKey(), d );
591 
592                     /*
593                      * TODO Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In
594                      * order to don't break backward-compat with those, only warn but don't error out.
595                      */
596                     validateEnum( prefix + "scope", problems, Severity.WARNING, Version.V20, d.getScope(),
597                                   d.getManagementKey(), d, "provided", "compile", "runtime", "test", "system" );
598 
599                     validateEffectiveModelAgainstDependency( prefix, problems, m, d, request );
600                 }
601                 else
602                 {
603                     validateEnum( prefix + "scope", problems, Severity.WARNING, Version.V20, d.getScope(),
604                                   d.getManagementKey(), d, "provided", "compile", "runtime", "test", "system",
605                                   "import" );
606                 }
607             }
608         }
609     }
610 
611     private void validateEffectiveModelAgainstDependency( String prefix, ModelProblemCollector problems, Model m,
612                                                           Dependency d, ModelBuildingRequest request )
613     {
614         String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion()
615                 + ( d.getClassifier() != null ? ":" + d.getClassifier() : ""  );
616         String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
617         if ( key.equals( mKey ) )
618         {
619             // This means a module which is build has a dependency which has the same
620             // groupId, artifactId, version and classifier coordinates. This is in consequence
621             // a self reference or in other words a circular reference which can not being resolved.
622             addViolation( problems, Severity.FATAL, Version.V31, prefix + " " + key, key, "is referencing itself.", d );
623 
624         }
625 
626     }
627 
628     private void validate20EffectivePluginDependencies( ModelProblemCollector problems, Plugin plugin,
629                                                         ModelBuildingRequest request )
630     {
631         List<Dependency> dependencies = plugin.getDependencies();
632 
633         if ( !dependencies.isEmpty() )
634         {
635             String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
636 
637             Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
638 
639             for ( Dependency d : dependencies )
640             {
641                 validateEffectiveDependency( problems, d, false, prefix, request );
642 
643                 validateVersion( prefix + "version", problems, errOn30, Version.BASE, d.getVersion(),
644                                  d.getManagementKey(), d );
645 
646                 validateEnum( prefix + "scope", problems, errOn30, Version.BASE, d.getScope(), d.getManagementKey(), d,
647                               "compile", "runtime", "system" );
648             }
649         }
650     }
651 
652     private void validateEffectiveDependency( ModelProblemCollector problems, Dependency d, boolean management,
653                                               String prefix, ModelBuildingRequest request )
654     {
655         validateId( prefix + "artifactId", problems, Severity.ERROR, Version.BASE, d.getArtifactId(),
656                     d.getManagementKey(), d );
657 
658         validateId( prefix + "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(),
659                     d );
660 
661         if ( !management )
662         {
663             validateStringNotEmpty( prefix + "type", problems, Severity.ERROR, Version.BASE, d.getType(),
664                                     d.getManagementKey(), d );
665 
666             validateDependencyVersion( problems, d, prefix );
667         }
668 
669         if ( "system".equals( d.getScope() ) )
670         {
671             String systemPath = d.getSystemPath();
672 
673             if ( StringUtils.isEmpty( systemPath ) )
674             {
675                 addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
676                               "is missing.", d );
677             }
678             else
679             {
680                 File sysFile = new File( systemPath );
681                 if ( !sysFile.isAbsolute() )
682                 {
683                     addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
684                                   "must specify an absolute path but is " + systemPath, d );
685                 }
686                 else if ( !sysFile.isFile() )
687                 {
688                     String msg = "refers to a non-existing file " + sysFile.getAbsolutePath();
689                     systemPath = systemPath.replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
690                     String jdkHome =
691                         request.getSystemProperties().getProperty( "java.home", "" ) + File.separator + "..";
692                     if ( systemPath.startsWith( jdkHome ) )
693                     {
694                         msg += ". Please verify that you run Maven using a JDK and not just a JRE.";
695                     }
696                     addViolation( problems, Severity.WARNING, Version.BASE, prefix + "systemPath", d.getManagementKey(),
697                                   msg, d );
698                 }
699             }
700         }
701         else if ( StringUtils.isNotEmpty( d.getSystemPath() ) )
702         {
703             addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
704                           "must be omitted." + " This field may only be specified for a dependency with system scope.",
705                           d );
706         }
707 
708         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
709         {
710             for ( Exclusion exclusion : d.getExclusions() )
711             {
712                 if ( request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 )
713                 {
714                     validateId( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING, Version.V20,
715                                 exclusion.getGroupId(), d.getManagementKey(), exclusion );
716 
717                     validateId( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING, Version.V20,
718                                 exclusion.getArtifactId(), d.getManagementKey(), exclusion );
719                 }
720                 else
721                 {
722                     validateIdWithWildcards( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING,
723                                              Version.V30, exclusion.getGroupId(), d.getManagementKey(), exclusion );
724 
725                     validateIdWithWildcards( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING,
726                                              Version.V30, exclusion.getArtifactId(), d.getManagementKey(), exclusion );
727                 }
728             }
729         }
730     }
731 
732     /**
733      * @since 3.2.4
734      */
735     protected void validateDependencyVersion( ModelProblemCollector problems, Dependency d, String prefix )
736     {
737         validateStringNotEmpty( prefix + "version", problems, Severity.ERROR, Version.BASE, d.getVersion(),
738                                 d.getManagementKey(), d );
739     }
740 
741     private void validateRawRepositories( ModelProblemCollector problems, List<Repository> repositories, String prefix,
742                                           ModelBuildingRequest request )
743     {
744         Map<String, Repository> index = new HashMap<>();
745 
746         for ( Repository repository : repositories )
747         {
748             validateStringNotEmpty( prefix + ".id", problems, Severity.ERROR, Version.V20, repository.getId(),
749                                     repository );
750 
751             validateStringNotEmpty( prefix + "[" + repository.getId() + "].url", problems, Severity.ERROR, Version.V20,
752                                     repository.getUrl(), repository );
753 
754             String key = repository.getId();
755 
756             Repository existing = index.get( key );
757 
758             if ( existing != null )
759             {
760                 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
761 
762                 addViolation( problems, errOn30, Version.V20, prefix + ".id", null, "must be unique: "
763                     + repository.getId() + " -> " + existing.getUrl() + " vs " + repository.getUrl(), repository );
764             }
765             else
766             {
767                 index.put( key, repository );
768             }
769         }
770     }
771 
772     private void validate20EffectiveRepository( ModelProblemCollector problems, Repository repository, String prefix,
773                                                 ModelBuildingRequest request )
774     {
775         if ( repository != null )
776         {
777             Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
778 
779             validateBannedCharacters( prefix + ".id", problems, errOn31, Version.V20, repository.getId(), null,
780                                       repository, ILLEGAL_REPO_ID_CHARS );
781 
782             if ( "local".equals( repository.getId() ) )
783             {
784                 addViolation( problems, errOn31, Version.V20, prefix + ".id", null,
785                               "must not be 'local'" + ", this identifier is reserved for the local repository"
786                                   + ", using it for other repositories will corrupt your repository metadata.",
787                               repository );
788             }
789 
790             if ( "legacy".equals( repository.getLayout() ) )
791             {
792                 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".layout", repository.getId(),
793                               "uses the unsupported value 'legacy', artifact resolution might fail.", repository );
794             }
795         }
796     }
797 
798     private void validate20RawResources( ModelProblemCollector problems, List<Resource> resources, String prefix,
799                                          ModelBuildingRequest request )
800     {
801         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
802 
803         for ( Resource resource : resources )
804         {
805             validateStringNotEmpty( prefix + ".directory", problems, Severity.ERROR, Version.V20,
806                                     resource.getDirectory(), resource );
807 
808             validateBoolean( prefix + ".filtering", problems, errOn30, Version.V20, resource.getFiltering(),
809                              resource.getDirectory(), resource );
810         }
811     }
812 
813     // ----------------------------------------------------------------------
814     // Field validation
815     // ----------------------------------------------------------------------
816 
817     private boolean validateId( String fieldName, ModelProblemCollector problems, String id,
818                                 InputLocationTracker tracker )
819     {
820         return validateId( fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker );
821     }
822 
823     private boolean validateId( String fieldName, ModelProblemCollector problems, Severity severity, Version version,
824                                 String id, String sourceHint, InputLocationTracker tracker )
825     {
826         if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) )
827         {
828             return false;
829         }
830         else
831         {
832             boolean match = ID_REGEX.matcher( id ).matches();
833             if ( !match )
834             {
835                 addViolation( problems, severity, version, fieldName, sourceHint,
836                               "with value '" + id + "' does not match a valid id pattern.", tracker );
837             }
838             return match;
839         }
840     }
841 
842     private boolean validateIdWithWildcards( String fieldName, ModelProblemCollector problems, Severity severity,
843                                              Version version, String id, String sourceHint,
844                                              InputLocationTracker tracker )
845     {
846         if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) )
847         {
848             return false;
849         }
850         else
851         {
852             boolean match = ID_WITH_WILDCARDS_REGEX.matcher( id ).matches();
853             if ( !match )
854             {
855                 addViolation( problems, severity, version, fieldName, sourceHint,
856                               "with value '" + id + "' does not match a valid id pattern.", tracker );
857             }
858             return match;
859         }
860     }
861 
862     private boolean validateStringNoExpression( String fieldName, ModelProblemCollector problems, Severity severity,
863                                                 Version version, String string, InputLocationTracker tracker )
864     {
865         if ( !hasExpression( string ) )
866         {
867             return true;
868         }
869 
870         addViolation( problems, severity, version, fieldName, null, "contains an expression but should be a constant.",
871                       tracker );
872 
873         return false;
874     }
875 
876     private boolean validateVersionNoExpression( String fieldName, ModelProblemCollector problems, Severity severity,
877                                                  Version version, String string, InputLocationTracker tracker )
878     {
879         if ( !hasExpression( string ) )
880         {
881             return true;
882         }
883 
884         //
885         // Acceptable versions for continuous delivery
886         //
887         // changelist
888         // revision
889         // sha1
890         //
891         Matcher m = CI_FRIENDLY_EXPRESSION.matcher( string.trim() );
892         while ( m.find() )
893         {
894             if ( !CI_FRIENDLY_POSSIBLE_PROPERTY_NAMES.contains( m.group( 1 ) ) )
895             {
896                 addViolation( problems, severity, version, fieldName, null,
897                               "contains an expression but should be a constant.", tracker );
898 
899                 return false;
900             }
901         }
902 
903         return true;
904     }
905 
906     private boolean hasExpression( String value )
907     {
908         return value != null && value.contains( "${" );
909     }
910 
911     private boolean hasProjectExpression( String value )
912     {
913         return value != null && value.contains( "${project." );
914     }
915 
916     private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity,
917                                             Version version, String string, InputLocationTracker tracker )
918     {
919         return validateStringNotEmpty( fieldName, problems, severity, version, string, null, tracker );
920     }
921 
922     /**
923      * Asserts:
924      * <p/>
925      * <ul>
926      * <li><code>string != null</code>
927      * <li><code>string.length > 0</code>
928      * </ul>
929      */
930     private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity,
931                                             Version version, String string, String sourceHint,
932                                             InputLocationTracker tracker )
933     {
934         if ( !validateNotNull( fieldName, problems, severity, version, string, sourceHint, tracker ) )
935         {
936             return false;
937         }
938 
939         if ( string.length() > 0 )
940         {
941             return true;
942         }
943 
944         addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker );
945 
946         return false;
947     }
948 
949     /**
950      * Asserts:
951      * <p/>
952      * <ul>
953      * <li><code>string != null</code>
954      * </ul>
955      */
956     private boolean validateNotNull( String fieldName, ModelProblemCollector problems, Severity severity,
957                                      Version version, Object object, String sourceHint, InputLocationTracker tracker )
958     {
959         if ( object != null )
960         {
961             return true;
962         }
963 
964         addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker );
965 
966         return false;
967     }
968 
969     private boolean validateBoolean( String fieldName, ModelProblemCollector problems, Severity severity,
970                                      Version version, String string, String sourceHint, InputLocationTracker tracker )
971     {
972         if ( string == null || string.length() <= 0 )
973         {
974             return true;
975         }
976 
977         if ( "true".equalsIgnoreCase( string ) || "false".equalsIgnoreCase( string ) )
978         {
979             return true;
980         }
981 
982         addViolation( problems, severity, version, fieldName, sourceHint,
983                       "must be 'true' or 'false' but is '" + string + "'.", tracker );
984 
985         return false;
986     }
987 
988     @SuppressWarnings( "checkstyle:parameternumber" )
989     private boolean validateEnum( String fieldName, ModelProblemCollector problems, Severity severity, Version version,
990                                   String string, String sourceHint, InputLocationTracker tracker,
991                                   String... validValues )
992     {
993         if ( string == null || string.length() <= 0 )
994         {
995             return true;
996         }
997 
998         List<String> values = Arrays.asList( validValues );
999 
1000         if ( values.contains( string ) )
1001         {
1002             return true;
1003         }
1004 
1005         addViolation( problems, severity, version, fieldName, sourceHint,
1006                       "must be one of " + values + " but is '" + string + "'.", tracker );
1007 
1008         return false;
1009     }
1010 
1011     @SuppressWarnings( "checkstyle:parameternumber" )
1012     private boolean validateBannedCharacters( String fieldName, ModelProblemCollector problems, Severity severity,
1013                                               Version version, String string, String sourceHint,
1014                                               InputLocationTracker tracker, String banned )
1015     {
1016         if ( string != null )
1017         {
1018             for ( int i = string.length() - 1; i >= 0; i-- )
1019             {
1020                 if ( banned.indexOf( string.charAt( i ) ) >= 0 )
1021                 {
1022                     addViolation( problems, severity, version, fieldName, sourceHint,
1023                                   "must not contain any of these characters " + banned + " but found "
1024                                       + string.charAt( i ),
1025                                   tracker );
1026                     return false;
1027                 }
1028             }
1029         }
1030 
1031         return true;
1032     }
1033 
1034     private boolean validateVersion( String fieldName, ModelProblemCollector problems, Severity severity,
1035                                      Version version, String string, String sourceHint, InputLocationTracker tracker )
1036     {
1037         if ( string == null || string.length() <= 0 )
1038         {
1039             return true;
1040         }
1041 
1042         if ( hasExpression( string ) )
1043         {
1044             addViolation( problems, severity, version, fieldName, sourceHint,
1045                           "must be a valid version but is '" + string + "'.", tracker );
1046             return false;
1047         }
1048 
1049         return validateBannedCharacters( fieldName, problems, severity, version, string, sourceHint, tracker,
1050                                          ILLEGAL_VERSION_CHARS );
1051 
1052     }
1053 
1054     private boolean validate20ProperSnapshotVersion( String fieldName, ModelProblemCollector problems,
1055                                                      Severity severity, Version version, String string,
1056                                                      String sourceHint, InputLocationTracker tracker )
1057     {
1058         if ( string == null || string.length() <= 0 )
1059         {
1060             return true;
1061         }
1062 
1063         if ( string.endsWith( "SNAPSHOT" ) && !string.endsWith( "-SNAPSHOT" ) )
1064         {
1065             addViolation( problems, severity, version, fieldName, sourceHint,
1066                           "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.", tracker );
1067             return false;
1068         }
1069 
1070         return true;
1071     }
1072 
1073     private boolean validate20PluginVersion( String fieldName, ModelProblemCollector problems, String string,
1074                                              String sourceHint, InputLocationTracker tracker,
1075                                              ModelBuildingRequest request )
1076     {
1077         if ( string == null )
1078         {
1079             // NOTE: The check for missing plugin versions is handled directly by the model builder
1080             return true;
1081         }
1082 
1083         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
1084 
1085         if ( !validateVersion( fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker ) )
1086         {
1087             return false;
1088         }
1089 
1090         if ( string.length() <= 0 || "RELEASE".equals( string ) || "LATEST".equals( string ) )
1091         {
1092             addViolation( problems, errOn30, Version.V20, fieldName, sourceHint,
1093                           "must be a valid version but is '" + string + "'.", tracker );
1094             return false;
1095         }
1096 
1097         return true;
1098     }
1099 
1100     private static void addViolation( ModelProblemCollector problems, Severity severity, Version version,
1101                                       String fieldName, String sourceHint, String message,
1102                                       InputLocationTracker tracker )
1103     {
1104         StringBuilder buffer = new StringBuilder( 256 );
1105         buffer.append( '\'' ).append( fieldName ).append( '\'' );
1106 
1107         if ( sourceHint != null )
1108         {
1109             buffer.append( " for " ).append( sourceHint );
1110         }
1111 
1112         buffer.append( ' ' ).append( message );
1113 
1114         // CHECKSTYLE_OFF: LineLength
1115         problems.add( new ModelProblemCollectorRequest( severity, version ).setMessage(
1116                                                                                         buffer.toString() ).setLocation( getLocation( fieldName, tracker ) ) );
1117         // CHECKSTYLE_ON: LineLength
1118     }
1119 
1120     private static InputLocation getLocation( String fieldName, InputLocationTracker tracker )
1121     {
1122         InputLocation location = null;
1123 
1124         if ( tracker != null )
1125         {
1126             if ( fieldName != null )
1127             {
1128                 Object key = fieldName;
1129 
1130                 int idx = fieldName.lastIndexOf( '.' );
1131                 if ( idx >= 0 )
1132                 {
1133                     fieldName = fieldName.substring( idx + 1 );
1134                     key = fieldName;
1135                 }
1136 
1137                 if ( fieldName.endsWith( "]" ) )
1138                 {
1139                     key = fieldName.substring( fieldName.lastIndexOf( '[' ) + 1, fieldName.length() - 1 );
1140                     try
1141                     {
1142                         key = Integer.valueOf( key.toString() );
1143                     }
1144                     catch ( NumberFormatException e )
1145                     {
1146                         // use key as is
1147                     }
1148                 }
1149 
1150                 location = tracker.getLocation( key );
1151             }
1152 
1153             if ( location == null )
1154             {
1155                 location = tracker.getLocation( "" );
1156             }
1157         }
1158 
1159         return location;
1160     }
1161 
1162     private static boolean equals( String s1, String s2 )
1163     {
1164         return StringUtils.clean( s1 ).equals( StringUtils.clean( s2 ) );
1165     }
1166 
1167     private static Severity getSeverity( ModelBuildingRequest request, int errorThreshold )
1168     {
1169         return getSeverity( request.getValidationLevel(), errorThreshold );
1170     }
1171 
1172     private static Severity getSeverity( int validationLevel, int errorThreshold )
1173     {
1174         if ( validationLevel < errorThreshold )
1175         {
1176             return Severity.WARNING;
1177         }
1178         else
1179         {
1180             return Severity.ERROR;
1181         }
1182     }
1183 
1184 }