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