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