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