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