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