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