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