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