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