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