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