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