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