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