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