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             }
602         }
603     }
604 
605     private void validateEffectiveModelAgainstDependency( String prefix, ModelProblemCollector problems, Model m,
606                                                           Dependency d, ModelBuildingRequest request )
607     {
608         String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion()
609                 + ( d.getClassifier() != null ? ":" + d.getClassifier() : ""  );
610         String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
611         if ( key.equals( mKey ) )
612         {
613             // This means a module which is build has a dependency which has the same
614             // groupId, artifactId, version and classifier coordinates. This is in consequence
615             // a self reference or in other words a circular reference which can not being resolved.
616             addViolation( problems, Severity.FATAL, Version.V31, prefix + " " + key, key, "is referencing itself.", d );
617 
618         }
619 
620     }
621 
622     private void validate20EffectivePluginDependencies( ModelProblemCollector problems, Plugin plugin,
623                                                         ModelBuildingRequest request )
624     {
625         List<Dependency> dependencies = plugin.getDependencies();
626 
627         if ( !dependencies.isEmpty() )
628         {
629             String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
630 
631             Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
632 
633             for ( Dependency d : dependencies )
634             {
635                 validateEffectiveDependency( problems, d, false, prefix, request );
636 
637                 validateVersion( prefix + "version", problems, errOn30, Version.BASE, d.getVersion(),
638                                  d.getManagementKey(), d );
639 
640                 validateEnum( prefix + "scope", problems, errOn30, Version.BASE, d.getScope(), d.getManagementKey(), d,
641                               "compile", "runtime", "system" );
642             }
643         }
644     }
645 
646     private void validateEffectiveDependency( ModelProblemCollector problems, Dependency d, boolean management,
647                                               String prefix, ModelBuildingRequest request )
648     {
649         validateId( prefix + "artifactId", problems, Severity.ERROR, Version.BASE, d.getArtifactId(),
650                     d.getManagementKey(), d );
651 
652         validateId( prefix + "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(),
653                     d );
654 
655         if ( !management )
656         {
657             validateStringNotEmpty( prefix + "type", problems, Severity.ERROR, Version.BASE, d.getType(),
658                                     d.getManagementKey(), d );
659 
660             validateDependencyVersion( problems, d, prefix );
661         }
662 
663         if ( "system".equals( d.getScope() ) )
664         {
665             String systemPath = d.getSystemPath();
666 
667             if ( StringUtils.isEmpty( systemPath ) )
668             {
669                 addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
670                               "is missing.", d );
671             }
672             else
673             {
674                 File sysFile = new File( systemPath );
675                 if ( !sysFile.isAbsolute() )
676                 {
677                     addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
678                                   "must specify an absolute path but is " + systemPath, d );
679                 }
680                 else if ( !sysFile.isFile() )
681                 {
682                     String msg = "refers to a non-existing file " + sysFile.getAbsolutePath();
683                     systemPath = systemPath.replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
684                     String jdkHome =
685                         request.getSystemProperties().getProperty( "java.home", "" ) + File.separator + "..";
686                     if ( systemPath.startsWith( jdkHome ) )
687                     {
688                         msg += ". Please verify that you run Maven using a JDK and not just a JRE.";
689                     }
690                     addViolation( problems, Severity.WARNING, Version.BASE, prefix + "systemPath", d.getManagementKey(),
691                                   msg, d );
692                 }
693             }
694         }
695         else if ( StringUtils.isNotEmpty( d.getSystemPath() ) )
696         {
697             addViolation( problems, Severity.ERROR, Version.BASE, prefix + "systemPath", d.getManagementKey(),
698                           "must be omitted." + " This field may only be specified for a dependency with system scope.",
699                           d );
700         }
701 
702         if ( request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 )
703         {
704             for ( Exclusion exclusion : d.getExclusions() )
705             {
706                 if ( request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 )
707                 {
708                     validateId( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING, Version.V20,
709                                 exclusion.getGroupId(), d.getManagementKey(), exclusion );
710 
711                     validateId( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING, Version.V20,
712                                 exclusion.getArtifactId(), d.getManagementKey(), exclusion );
713                 }
714                 else
715                 {
716                     validateIdWithWildcards( prefix + "exclusions.exclusion.groupId", problems, Severity.WARNING,
717                                              Version.V30, exclusion.getGroupId(), d.getManagementKey(), exclusion );
718 
719                     validateIdWithWildcards( prefix + "exclusions.exclusion.artifactId", problems, Severity.WARNING,
720                                              Version.V30, exclusion.getArtifactId(), d.getManagementKey(), exclusion );
721                 }
722             }
723         }
724     }
725 
726     /**
727      * @since 3.2.4
728      */
729     protected void validateDependencyVersion( ModelProblemCollector problems, Dependency d, String prefix )
730     {
731         validateStringNotEmpty( prefix + "version", problems, Severity.ERROR, Version.BASE, d.getVersion(),
732                                 d.getManagementKey(), d );
733     }
734 
735     private void validateRawRepositories( ModelProblemCollector problems, List<Repository> repositories, String prefix,
736                                           ModelBuildingRequest request )
737     {
738         Map<String, Repository> index = new HashMap<>();
739 
740         for ( Repository repository : repositories )
741         {
742             validateStringNotEmpty( prefix + ".id", problems, Severity.ERROR, Version.V20, repository.getId(),
743                                     repository );
744 
745             validateStringNotEmpty( prefix + "[" + repository.getId() + "].url", problems, Severity.ERROR, Version.V20,
746                                     repository.getUrl(), repository );
747 
748             String key = repository.getId();
749 
750             Repository existing = index.get( key );
751 
752             if ( existing != null )
753             {
754                 Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
755 
756                 addViolation( problems, errOn30, Version.V20, prefix + ".id", null, "must be unique: "
757                     + repository.getId() + " -> " + existing.getUrl() + " vs " + repository.getUrl(), repository );
758             }
759             else
760             {
761                 index.put( key, repository );
762             }
763         }
764     }
765 
766     private void validate20EffectiveRepository( ModelProblemCollector problems, Repository repository, String prefix,
767                                                 ModelBuildingRequest request )
768     {
769         if ( repository != null )
770         {
771             Severity errOn31 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1 );
772 
773             validateBannedCharacters( prefix + ".id", problems, errOn31, Version.V20, repository.getId(), null,
774                                       repository, ILLEGAL_REPO_ID_CHARS );
775 
776             if ( "local".equals( repository.getId() ) )
777             {
778                 addViolation( problems, errOn31, Version.V20, prefix + ".id", null,
779                               "must not be 'local'" + ", this identifier is reserved for the local repository"
780                                   + ", using it for other repositories will corrupt your repository metadata.",
781                               repository );
782             }
783 
784             if ( "legacy".equals( repository.getLayout() ) )
785             {
786                 addViolation( problems, Severity.WARNING, Version.V20, prefix + ".layout", repository.getId(),
787                               "uses the unsupported value 'legacy', artifact resolution might fail.", repository );
788             }
789         }
790     }
791 
792     private void validate20RawResources( ModelProblemCollector problems, List<Resource> resources, String prefix,
793                                          ModelBuildingRequest request )
794     {
795         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
796 
797         for ( Resource resource : resources )
798         {
799             validateStringNotEmpty( prefix + ".directory", problems, Severity.ERROR, Version.V20,
800                                     resource.getDirectory(), resource );
801 
802             validateBoolean( prefix + ".filtering", problems, errOn30, Version.V20, resource.getFiltering(),
803                              resource.getDirectory(), resource );
804         }
805     }
806 
807     // ----------------------------------------------------------------------
808     // Field validation
809     // ----------------------------------------------------------------------
810 
811     private boolean validateId( String fieldName, ModelProblemCollector problems, String id,
812                                 InputLocationTracker tracker )
813     {
814         return validateId( fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker );
815     }
816 
817     private boolean validateId( String fieldName, ModelProblemCollector problems, Severity severity, Version version,
818                                 String id, String sourceHint, InputLocationTracker tracker )
819     {
820         if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) )
821         {
822             return false;
823         }
824         else
825         {
826             boolean match = ID_REGEX.matcher( id ).matches();
827             if ( !match )
828             {
829                 addViolation( problems, severity, version, fieldName, sourceHint,
830                               "with value '" + id + "' does not match a valid id pattern.", tracker );
831             }
832             return match;
833         }
834     }
835 
836     private boolean validateIdWithWildcards( String fieldName, ModelProblemCollector problems, Severity severity,
837                                              Version version, String id, String sourceHint,
838                                              InputLocationTracker tracker )
839     {
840         if ( !validateStringNotEmpty( fieldName, problems, severity, version, id, sourceHint, tracker ) )
841         {
842             return false;
843         }
844         else
845         {
846             boolean match = ID_WITH_WILDCARDS_REGEX.matcher( id ).matches();
847             if ( !match )
848             {
849                 addViolation( problems, severity, version, fieldName, sourceHint,
850                               "with value '" + id + "' does not match a valid id pattern.", tracker );
851             }
852             return match;
853         }
854     }
855 
856     private boolean validateStringNoExpression( String fieldName, ModelProblemCollector problems, Severity severity,
857                                                 Version version, String string, InputLocationTracker tracker )
858     {
859         if ( !hasExpression( string ) )
860         {
861             return true;
862         }
863 
864         addViolation( problems, severity, version, fieldName, null, "contains an expression but should be a constant.",
865                       tracker );
866 
867         return false;
868     }
869 
870     private boolean validateVersionNoExpression( String fieldName, ModelProblemCollector problems, Severity severity,
871                                                  Version version, String string, InputLocationTracker tracker )
872     {
873         if ( !hasExpression( string ) )
874         {
875             return true;
876         }
877 
878         //
879         // Acceptable versions for continuous delivery
880         //
881         // changelist
882         // revision
883         // sha1
884         //
885         Matcher m = CI_FRIENDLY_EXPRESSION.matcher( string.trim() );
886         while ( m.find() )
887         {
888             if ( !CI_FRIENDLY_POSSIBLE_PROPERTY_NAMES.contains( m.group( 1 ) ) )
889             {
890                 addViolation( problems, severity, version, fieldName, null,
891                               "contains an expression but should be a constant.", tracker );
892 
893                 return false;
894             }
895         }
896 
897         return true;
898     }
899 
900     private boolean hasExpression( String value )
901     {
902         return value != null && value.contains( "${" );
903     }
904 
905     private boolean hasProjectExpression( String value )
906     {
907         return value != null && value.contains( "${project." );
908     }
909 
910     private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity,
911                                             Version version, String string, InputLocationTracker tracker )
912     {
913         return validateStringNotEmpty( fieldName, problems, severity, version, string, null, tracker );
914     }
915 
916     /**
917      * Asserts:
918      * <p/>
919      * <ul>
920      * <li><code>string != null</code>
921      * <li><code>string.length > 0</code>
922      * </ul>
923      */
924     private boolean validateStringNotEmpty( String fieldName, ModelProblemCollector problems, Severity severity,
925                                             Version version, String string, String sourceHint,
926                                             InputLocationTracker tracker )
927     {
928         if ( !validateNotNull( fieldName, problems, severity, version, string, sourceHint, tracker ) )
929         {
930             return false;
931         }
932 
933         if ( string.length() > 0 )
934         {
935             return true;
936         }
937 
938         addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker );
939 
940         return false;
941     }
942 
943     /**
944      * Asserts:
945      * <p/>
946      * <ul>
947      * <li><code>string != null</code>
948      * </ul>
949      */
950     private boolean validateNotNull( String fieldName, ModelProblemCollector problems, Severity severity,
951                                      Version version, Object object, String sourceHint, InputLocationTracker tracker )
952     {
953         if ( object != null )
954         {
955             return true;
956         }
957 
958         addViolation( problems, severity, version, fieldName, sourceHint, "is missing.", tracker );
959 
960         return false;
961     }
962 
963     private boolean validateBoolean( String fieldName, ModelProblemCollector problems, Severity severity,
964                                      Version version, String string, String sourceHint, InputLocationTracker tracker )
965     {
966         if ( string == null || string.length() <= 0 )
967         {
968             return true;
969         }
970 
971         if ( "true".equalsIgnoreCase( string ) || "false".equalsIgnoreCase( string ) )
972         {
973             return true;
974         }
975 
976         addViolation( problems, severity, version, fieldName, sourceHint,
977                       "must be 'true' or 'false' but is '" + string + "'.", tracker );
978 
979         return false;
980     }
981 
982     @SuppressWarnings( "checkstyle:parameternumber" )
983     private boolean validateEnum( String fieldName, ModelProblemCollector problems, Severity severity, Version version,
984                                   String string, String sourceHint, InputLocationTracker tracker,
985                                   String... validValues )
986     {
987         if ( string == null || string.length() <= 0 )
988         {
989             return true;
990         }
991 
992         List<String> values = Arrays.asList( validValues );
993 
994         if ( values.contains( string ) )
995         {
996             return true;
997         }
998 
999         addViolation( problems, severity, version, fieldName, sourceHint,
1000                       "must be one of " + values + " but is '" + string + "'.", tracker );
1001 
1002         return false;
1003     }
1004 
1005     @SuppressWarnings( "checkstyle:parameternumber" )
1006     private boolean validateBannedCharacters( String fieldName, ModelProblemCollector problems, Severity severity,
1007                                               Version version, String string, String sourceHint,
1008                                               InputLocationTracker tracker, String banned )
1009     {
1010         if ( string != null )
1011         {
1012             for ( int i = string.length() - 1; i >= 0; i-- )
1013             {
1014                 if ( banned.indexOf( string.charAt( i ) ) >= 0 )
1015                 {
1016                     addViolation( problems, severity, version, fieldName, sourceHint,
1017                                   "must not contain any of these characters " + banned + " but found "
1018                                       + string.charAt( i ),
1019                                   tracker );
1020                     return false;
1021                 }
1022             }
1023         }
1024 
1025         return true;
1026     }
1027 
1028     private boolean validateVersion( String fieldName, ModelProblemCollector problems, Severity severity,
1029                                      Version version, String string, String sourceHint, InputLocationTracker tracker )
1030     {
1031         if ( string == null || string.length() <= 0 )
1032         {
1033             return true;
1034         }
1035 
1036         if ( hasExpression( string ) )
1037         {
1038             addViolation( problems, severity, version, fieldName, sourceHint,
1039                           "must be a valid version but is '" + string + "'.", tracker );
1040             return false;
1041         }
1042 
1043         return validateBannedCharacters( fieldName, problems, severity, version, string, sourceHint, tracker,
1044                                          ILLEGAL_VERSION_CHARS );
1045 
1046     }
1047 
1048     private boolean validate20ProperSnapshotVersion( String fieldName, ModelProblemCollector problems,
1049                                                      Severity severity, Version version, String string,
1050                                                      String sourceHint, InputLocationTracker tracker )
1051     {
1052         if ( string == null || string.length() <= 0 )
1053         {
1054             return true;
1055         }
1056 
1057         if ( string.endsWith( "SNAPSHOT" ) && !string.endsWith( "-SNAPSHOT" ) )
1058         {
1059             addViolation( problems, severity, version, fieldName, sourceHint,
1060                           "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.", tracker );
1061             return false;
1062         }
1063 
1064         return true;
1065     }
1066 
1067     private boolean validate20PluginVersion( String fieldName, ModelProblemCollector problems, String string,
1068                                              String sourceHint, InputLocationTracker tracker,
1069                                              ModelBuildingRequest request )
1070     {
1071         if ( string == null )
1072         {
1073             // NOTE: The check for missing plugin versions is handled directly by the model builder
1074             return true;
1075         }
1076 
1077         Severity errOn30 = getSeverity( request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0 );
1078 
1079         if ( !validateVersion( fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker ) )
1080         {
1081             return false;
1082         }
1083 
1084         if ( string.length() <= 0 || "RELEASE".equals( string ) || "LATEST".equals( string ) )
1085         {
1086             addViolation( problems, errOn30, Version.V20, fieldName, sourceHint,
1087                           "must be a valid version but is '" + string + "'.", tracker );
1088             return false;
1089         }
1090 
1091         return true;
1092     }
1093 
1094     private static void addViolation( ModelProblemCollector problems, Severity severity, Version version,
1095                                       String fieldName, String sourceHint, String message,
1096                                       InputLocationTracker tracker )
1097     {
1098         StringBuilder buffer = new StringBuilder( 256 );
1099         buffer.append( '\'' ).append( fieldName ).append( '\'' );
1100 
1101         if ( sourceHint != null )
1102         {
1103             buffer.append( " for " ).append( sourceHint );
1104         }
1105 
1106         buffer.append( ' ' ).append( message );
1107 
1108         // CHECKSTYLE_OFF: LineLength
1109         problems.add( new ModelProblemCollectorRequest( severity, version ).setMessage(
1110                                                                                         buffer.toString() ).setLocation( getLocation( fieldName, tracker ) ) );
1111         // CHECKSTYLE_ON: LineLength
1112     }
1113 
1114     private static InputLocation getLocation( String fieldName, InputLocationTracker tracker )
1115     {
1116         InputLocation location = null;
1117 
1118         if ( tracker != null )
1119         {
1120             if ( fieldName != null )
1121             {
1122                 Object key = fieldName;
1123 
1124                 int idx = fieldName.lastIndexOf( '.' );
1125                 if ( idx >= 0 )
1126                 {
1127                     fieldName = fieldName.substring( idx + 1 );
1128                     key = fieldName;
1129                 }
1130 
1131                 if ( fieldName.endsWith( "]" ) )
1132                 {
1133                     key = fieldName.substring( fieldName.lastIndexOf( '[' ) + 1, fieldName.length() - 1 );
1134                     try
1135                     {
1136                         key = Integer.valueOf( key.toString() );
1137                     }
1138                     catch ( NumberFormatException e )
1139                     {
1140                         // use key as is
1141                     }
1142                 }
1143 
1144                 location = tracker.getLocation( key );
1145             }
1146 
1147             if ( location == null )
1148             {
1149                 location = tracker.getLocation( "" );
1150             }
1151         }
1152 
1153         return location;
1154     }
1155 
1156     private static boolean equals( String s1, String s2 )
1157     {
1158         return StringUtils.clean( s1 ).equals( StringUtils.clean( s2 ) );
1159     }
1160 
1161     private static Severity getSeverity( ModelBuildingRequest request, int errorThreshold )
1162     {
1163         return getSeverity( request.getValidationLevel(), errorThreshold );
1164     }
1165 
1166     private static Severity getSeverity( int validationLevel, int errorThreshold )
1167     {
1168         if ( validationLevel < errorThreshold )
1169         {
1170             return Severity.WARNING;
1171         }
1172         else
1173         {
1174             return Severity.ERROR;
1175         }
1176     }
1177 
1178 }