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