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