View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.model.validation;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.Set;
34  import java.util.regex.Matcher;
35  import java.util.regex.Pattern;
36  
37  import org.apache.maven.api.model.Activation;
38  import org.apache.maven.api.model.ActivationFile;
39  import org.apache.maven.api.model.Build;
40  import org.apache.maven.api.model.BuildBase;
41  import org.apache.maven.api.model.Dependency;
42  import org.apache.maven.api.model.DependencyManagement;
43  import org.apache.maven.api.model.DistributionManagement;
44  import org.apache.maven.api.model.Exclusion;
45  import org.apache.maven.api.model.InputLocation;
46  import org.apache.maven.api.model.InputLocationTracker;
47  import org.apache.maven.api.model.Parent;
48  import org.apache.maven.api.model.Plugin;
49  import org.apache.maven.api.model.PluginExecution;
50  import org.apache.maven.api.model.PluginManagement;
51  import org.apache.maven.api.model.Profile;
52  import org.apache.maven.api.model.ReportPlugin;
53  import org.apache.maven.api.model.Reporting;
54  import org.apache.maven.api.model.Repository;
55  import org.apache.maven.api.model.Resource;
56  import org.apache.maven.model.Model;
57  import org.apache.maven.model.building.ModelBuildingRequest;
58  import org.apache.maven.model.building.ModelProblem.Severity;
59  import org.apache.maven.model.building.ModelProblem.Version;
60  import org.apache.maven.model.building.ModelProblemCollector;
61  import org.apache.maven.model.building.ModelProblemCollectorRequest;
62  import org.apache.maven.model.interpolation.ModelVersionProcessor;
63  import org.apache.maven.model.v4.MavenModelVersion;
64  
65  /**
66   */
67  @Named
68  @Singleton
69  public class DefaultModelValidator implements ModelValidator {
70  
71      public static final List<String> VALID_MODEL_VERSIONS =
72              Collections.unmodifiableList(Arrays.asList("4.0.0", "4.1.0"));
73  
74      private static final Pattern EXPRESSION_NAME_PATTERN = Pattern.compile("\\$\\{(.+?)}");
75      private static final Pattern EXPRESSION_PROJECT_NAME_PATTERN = Pattern.compile("\\$\\{(project.+?)}");
76  
77      private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";
78  
79      private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;
80  
81      private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;
82  
83      private static final String EMPTY = "";
84  
85      private final Set<String> validCoordinateIds = new HashSet<>();
86  
87      private final Set<String> validProfileIds = new HashSet<>();
88  
89      private final ModelVersionProcessor versionProcessor;
90  
91      @Inject
92      public DefaultModelValidator(ModelVersionProcessor versionProcessor) {
93          this.versionProcessor = versionProcessor;
94      }
95  
96      @Override
97      public void validateFileModel(Model ma, ModelBuildingRequest request, ModelProblemCollector problems) {
98  
99          org.apache.maven.api.model.Model m = ma.getDelegate();
100 
101         Parent parent = m.getParent();
102         if (parent != null) {
103             validateStringNotEmpty(
104                     "parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);
105 
106             validateStringNotEmpty(
107                     "parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(), parent);
108 
109             if (equals(parent.getGroupId(), m.getGroupId()) && equals(parent.getArtifactId(), m.getArtifactId())) {
110                 addViolation(
111                         problems,
112                         Severity.FATAL,
113                         Version.BASE,
114                         "parent.artifactId",
115                         null,
116                         "must be changed"
117                                 + ", the parent element cannot have the same groupId:artifactId as the project.",
118                         parent);
119             }
120 
121             if (equals("LATEST", parent.getVersion()) || equals("RELEASE", parent.getVersion())) {
122                 addViolation(
123                         problems,
124                         Severity.WARNING,
125                         Version.BASE,
126                         "parent.version",
127                         null,
128                         "is either LATEST or RELEASE (both of them are being deprecated)",
129                         parent);
130             }
131         }
132 
133         if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
134             Set<String> modules = new HashSet<>();
135             for (int i = 0, n = m.getModules().size(); i < n; i++) {
136                 String module = m.getModules().get(i);
137                 if (!modules.add(module)) {
138                     addViolation(
139                             problems,
140                             Severity.ERROR,
141                             Version.V20,
142                             "modules.module[" + i + "]",
143                             null,
144                             "specifies duplicate child module " + module,
145                             m.getLocation("modules"));
146                 }
147             }
148 
149             Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
150 
151             // The file pom may not contain the modelVersion yet, as it may be set later by the
152             // ModelVersionXMLFilter.
153             if (m.getModelVersion() != null && !m.getModelVersion().isEmpty()) {
154                 validateModelVersion(problems, m.getModelVersion(), m, VALID_MODEL_VERSIONS);
155             }
156 
157             validateStringNoExpression("groupId", problems, Severity.WARNING, Version.V20, m.getGroupId(), m);
158             if (parent == null) {
159                 validateStringNotEmpty("groupId", problems, Severity.FATAL, Version.V20, m.getGroupId(), m);
160             }
161 
162             validateStringNoExpression("artifactId", problems, Severity.WARNING, Version.V20, m.getArtifactId(), m);
163             validateStringNotEmpty("artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m);
164 
165             validateVersionNoExpression("version", problems, Severity.WARNING, Version.V20, m.getVersion(), m);
166             if (parent == null) {
167                 validateStringNotEmpty("version", problems, Severity.FATAL, Version.V20, m.getVersion(), m);
168             }
169 
170             validate20RawDependencies(problems, m.getDependencies(), "dependencies.dependency.", EMPTY, request);
171 
172             validate20RawDependenciesSelfReferencing(
173                     problems, m, m.getDependencies(), "dependencies.dependency", request);
174 
175             if (m.getDependencyManagement() != null) {
176                 validate20RawDependencies(
177                         problems,
178                         m.getDependencyManagement().getDependencies(),
179                         "dependencyManagement.dependencies.dependency.",
180                         EMPTY,
181                         request);
182             }
183 
184             validateRawRepositories(problems, m.getRepositories(), "repositories.repository.", EMPTY, request);
185 
186             validateRawRepositories(
187                     problems, m.getPluginRepositories(), "pluginRepositories.pluginRepository.", EMPTY, request);
188 
189             Build build = m.getBuild();
190             if (build != null) {
191                 validate20RawPlugins(problems, build.getPlugins(), "build.plugins.plugin.", EMPTY, request);
192 
193                 PluginManagement mgmt = build.getPluginManagement();
194                 if (mgmt != null) {
195                     validate20RawPlugins(
196                             problems, mgmt.getPlugins(), "build.pluginManagement.plugins.plugin.", EMPTY, request);
197                 }
198             }
199 
200             Set<String> profileIds = new HashSet<>();
201 
202             for (Profile profile : m.getProfiles()) {
203                 String prefix = "profiles.profile[" + profile.getId() + "].";
204 
205                 validateProfileId(prefix, "id", problems, Severity.ERROR, Version.V40, profile.getId(), null, m);
206 
207                 if (!profileIds.add(profile.getId())) {
208                     addViolation(
209                             problems,
210                             errOn30,
211                             Version.V20,
212                             "profiles.profile.id",
213                             null,
214                             "must be unique but found duplicate profile with id " + profile.getId(),
215                             profile);
216                 }
217 
218                 validate30RawProfileActivation(problems, profile.getActivation(), prefix);
219 
220                 validate20RawDependencies(
221                         problems, profile.getDependencies(), prefix, "dependencies.dependency.", request);
222 
223                 if (profile.getDependencyManagement() != null) {
224                     validate20RawDependencies(
225                             problems,
226                             profile.getDependencyManagement().getDependencies(),
227                             prefix,
228                             "dependencyManagement.dependencies.dependency.",
229                             request);
230                 }
231 
232                 validateRawRepositories(
233                         problems, profile.getRepositories(), prefix, "repositories.repository.", request);
234 
235                 validateRawRepositories(
236                         problems,
237                         profile.getPluginRepositories(),
238                         prefix,
239                         "pluginRepositories.pluginRepository.",
240                         request);
241 
242                 BuildBase buildBase = profile.getBuild();
243                 if (buildBase != null) {
244                     validate20RawPlugins(problems, buildBase.getPlugins(), prefix, "plugins.plugin.", request);
245 
246                     PluginManagement mgmt = buildBase.getPluginManagement();
247                     if (mgmt != null) {
248                         validate20RawPlugins(
249                                 problems, mgmt.getPlugins(), prefix, "pluginManagement.plugins.plugin.", request);
250                     }
251                 }
252             }
253         }
254     }
255 
256     @Override
257     public void validateRawModel(Model ma, ModelBuildingRequest request, ModelProblemCollector problems) {
258         org.apache.maven.api.model.Model m = ma.getDelegate();
259 
260         // [MNG-6074] Maven should produce an error if no model version has been set in a POM file used to build an
261         // effective model.
262         //
263         // As of 3.4, the model version is mandatory even in raw models. The XML element still is optional in the
264         // XML schema and this will not change anytime soon. We do not want to build effective models based on
265         // models without a version starting with 3.4.
266         validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.V20, m.getModelVersion(), m);
267 
268         validateModelVersion(problems, m.getModelVersion(), m, VALID_MODEL_VERSIONS);
269 
270         String minVersion = new MavenModelVersion().getModelVersion(m);
271         if (m.getModelVersion() != null && compareModelVersions(minVersion, m.getModelVersion()) > 0) {
272             addViolation(
273                     problems,
274                     Severity.FATAL,
275                     Version.V40,
276                     "model",
277                     null,
278                     "the model contains elements that require a model version of " + minVersion,
279                     m);
280         }
281 
282         Parent parent = m.getParent();
283 
284         if (parent != null) {
285             validateStringNotEmpty(
286                     "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), parent);
287         }
288     }
289 
290     private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
291         if (activation == null || activation.getFile() == null) {
292             return;
293         }
294 
295         ActivationFile file = activation.getFile();
296 
297         String path;
298         String location;
299 
300         if (file.getExists() != null && !file.getExists().isEmpty()) {
301             path = file.getExists();
302             location = "exists";
303         } else if (file.getMissing() != null && !file.getMissing().isEmpty()) {
304             path = file.getMissing();
305             location = "missing";
306         } else {
307             return;
308         }
309 
310         if (hasProjectExpression(path)) {
311             Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(path);
312             while (matcher.find()) {
313                 String propertyName = matcher.group(0);
314                 if (!"${project.basedir}".equals(propertyName)) {
315                     addViolation(
316                             problems,
317                             Severity.WARNING,
318                             Version.V30,
319                             prefix + "activation.file." + location,
320                             null,
321                             "Failed to interpolate file location " + path + ": " + propertyName
322                                     + " expressions are not supported during profile activation.",
323                             file.getLocation(location));
324                 }
325             }
326         }
327     }
328 
329     private void validate20RawPlugins(
330             ModelProblemCollector problems,
331             List<Plugin> plugins,
332             String prefix,
333             String prefix2,
334             ModelBuildingRequest request) {
335         Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);
336 
337         Map<String, Plugin> index = new HashMap<>();
338 
339         for (Plugin plugin : plugins) {
340             if (plugin.getGroupId() == null
341                     || (plugin.getGroupId() != null
342                             && plugin.getGroupId().trim().isEmpty())) {
343                 addViolation(
344                         problems,
345                         Severity.FATAL,
346                         Version.V20,
347                         prefix + prefix2 + "(groupId:artifactId)",
348                         null,
349                         "groupId of a plugin must be defined. ",
350                         plugin);
351             }
352 
353             if (plugin.getArtifactId() == null
354                     || (plugin.getArtifactId() != null
355                             && plugin.getArtifactId().trim().isEmpty())) {
356                 addViolation(
357                         problems,
358                         Severity.FATAL,
359                         Version.V20,
360                         prefix + prefix2 + "(groupId:artifactId)",
361                         null,
362                         "artifactId of a plugin must be defined. ",
363                         plugin);
364             }
365 
366             // This will catch cases like <version></version> or <version/>
367             if (plugin.getVersion() != null && plugin.getVersion().trim().isEmpty()) {
368                 addViolation(
369                         problems,
370                         Severity.FATAL,
371                         Version.V20,
372                         prefix + prefix2 + "(groupId:artifactId)",
373                         null,
374                         "version of a plugin must be defined. ",
375                         plugin);
376             }
377 
378             String key = plugin.getKey();
379 
380             Plugin existing = index.get(key);
381 
382             if (existing != null) {
383                 addViolation(
384                         problems,
385                         errOn31,
386                         Version.V20,
387                         prefix + prefix2 + "(groupId:artifactId)",
388                         null,
389                         "must be unique but found duplicate declaration of plugin " + key,
390                         plugin);
391             } else {
392                 index.put(key, plugin);
393             }
394 
395             Set<String> executionIds = new HashSet<>();
396 
397             for (PluginExecution exec : plugin.getExecutions()) {
398                 if (!executionIds.add(exec.getId())) {
399                     addViolation(
400                             problems,
401                             Severity.ERROR,
402                             Version.V20,
403                             prefix + prefix2 + "[" + plugin.getKey() + "].executions.execution.id",
404                             null,
405                             "must be unique but found duplicate execution with id " + exec.getId(),
406                             exec);
407                 }
408             }
409         }
410     }
411 
412     @Override
413     public void validateEffectiveModel(Model ma, ModelBuildingRequest request, ModelProblemCollector problems) {
414         org.apache.maven.api.model.Model m = ma.getDelegate();
415 
416         validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, m.getModelVersion(), m);
417 
418         validateCoordinateId("groupId", problems, m.getGroupId(), m);
419 
420         validateCoordinateId("artifactId", problems, m.getArtifactId(), m);
421 
422         validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, m.getPackaging(), m);
423 
424         if (!m.getModules().isEmpty()) {
425             if (!"pom".equals(m.getPackaging())) {
426                 addViolation(
427                         problems,
428                         Severity.ERROR,
429                         Version.BASE,
430                         "packaging",
431                         null,
432                         "with value '" + m.getPackaging() + "' is invalid. Aggregator projects "
433                                 + "require 'pom' as packaging.",
434                         m);
435             }
436 
437             for (int i = 0, n = m.getModules().size(); i < n; i++) {
438                 String module = m.getModules().get(i);
439 
440                 boolean isBlankModule = true;
441                 if (module != null) {
442                     for (int j = 0; j < module.length(); j++) {
443                         if (!Character.isWhitespace(module.charAt(j))) {
444                             isBlankModule = false;
445                         }
446                     }
447                 }
448 
449                 if (isBlankModule) {
450                     addViolation(
451                             problems,
452                             Severity.ERROR,
453                             Version.BASE,
454                             "modules.module[" + i + "]",
455                             null,
456                             "has been specified without a path to the project directory.",
457                             m.getLocation("modules"));
458                 }
459             }
460         }
461 
462         validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, m.getVersion(), m);
463 
464         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
465 
466         validateEffectiveDependencies(problems, m, m.getDependencies(), false, request);
467 
468         DependencyManagement mgmt = m.getDependencyManagement();
469         if (mgmt != null) {
470             validateEffectiveDependencies(problems, m, mgmt.getDependencies(), true, request);
471         }
472 
473         if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
474             Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);
475 
476             validateBannedCharacters(
477                     EMPTY, "version", problems, errOn31, Version.V20, m.getVersion(), null, m, ILLEGAL_VERSION_CHARS);
478             validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, m.getVersion(), null, m);
479 
480             Build build = m.getBuild();
481             if (build != null) {
482                 for (Plugin p : build.getPlugins()) {
483                     validateStringNotEmpty(
484                             "build.plugins.plugin.artifactId",
485                             problems,
486                             Severity.ERROR,
487                             Version.V20,
488                             p.getArtifactId(),
489                             p);
490 
491                     validateStringNotEmpty(
492                             "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(), p);
493 
494                     validate20PluginVersion(
495                             "build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p, request);
496 
497                     validateBoolean(
498                             "build.plugins.plugin.inherited",
499                             EMPTY,
500                             problems,
501                             errOn30,
502                             Version.V20,
503                             p.getInherited(),
504                             p.getKey(),
505                             p);
506 
507                     validateBoolean(
508                             "build.plugins.plugin.extensions",
509                             EMPTY,
510                             problems,
511                             errOn30,
512                             Version.V20,
513                             p.getExtensions(),
514                             p.getKey(),
515                             p);
516 
517                     validate20EffectivePluginDependencies(problems, p, request);
518                 }
519 
520                 validate20RawResources(problems, build.getResources(), "build.resources.resource.", request);
521 
522                 validate20RawResources(
523                         problems, build.getTestResources(), "build.testResources.testResource.", request);
524             }
525 
526             Reporting reporting = m.getReporting();
527             if (reporting != null) {
528                 for (ReportPlugin p : reporting.getPlugins()) {
529                     validateStringNotEmpty(
530                             "reporting.plugins.plugin.artifactId",
531                             problems,
532                             Severity.ERROR,
533                             Version.V20,
534                             p.getArtifactId(),
535                             p);
536 
537                     validateStringNotEmpty(
538                             "reporting.plugins.plugin.groupId",
539                             problems,
540                             Severity.ERROR,
541                             Version.V20,
542                             p.getGroupId(),
543                             p);
544                 }
545             }
546 
547             for (Repository repository : m.getRepositories()) {
548                 validate20EffectiveRepository(problems, repository, "repositories.repository.", request);
549             }
550 
551             for (Repository repository : m.getPluginRepositories()) {
552                 validate20EffectiveRepository(problems, repository, "pluginRepositories.pluginRepository.", request);
553             }
554 
555             DistributionManagement distMgmt = m.getDistributionManagement();
556             if (distMgmt != null) {
557                 if (distMgmt.getStatus() != null) {
558                     addViolation(
559                             problems,
560                             Severity.ERROR,
561                             Version.V20,
562                             "distributionManagement.status",
563                             null,
564                             "must not be specified.",
565                             distMgmt);
566                 }
567 
568                 validate20EffectiveRepository(
569                         problems, distMgmt.getRepository(), "distributionManagement.repository.", request);
570                 validate20EffectiveRepository(
571                         problems,
572                         distMgmt.getSnapshotRepository(),
573                         "distributionManagement.snapshotRepository.",
574                         request);
575             }
576         }
577     }
578 
579     private void validate20RawDependencies(
580             ModelProblemCollector problems,
581             List<Dependency> dependencies,
582             String prefix,
583             String prefix2,
584             ModelBuildingRequest request) {
585         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
586         Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);
587 
588         Map<String, Dependency> index = new HashMap<>();
589 
590         for (Dependency dependency : dependencies) {
591             String key = dependency.getManagementKey();
592 
593             if ("import".equals(dependency.getScope())) {
594                 if (!"pom".equals(dependency.getType())) {
595                     addViolation(
596                             problems,
597                             Severity.WARNING,
598                             Version.V20,
599                             prefix + prefix2 + "type",
600                             key,
601                             "must be 'pom' to import the managed dependencies.",
602                             dependency);
603                 } else if (dependency.getClassifier() != null
604                         && !dependency.getClassifier().isEmpty()) {
605                     addViolation(
606                             problems,
607                             errOn30,
608                             Version.V20,
609                             prefix + prefix2 + "classifier",
610                             key,
611                             "must be empty, imported POM cannot have a classifier.",
612                             dependency);
613                 }
614             } else if ("system".equals(dependency.getScope())) {
615 
616                 if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1) {
617                     addViolation(
618                             problems,
619                             Severity.WARNING,
620                             Version.V31,
621                             prefix + prefix2 + "scope",
622                             key,
623                             "declares usage of deprecated 'system' scope ",
624                             dependency);
625                 }
626 
627                 String sysPath = dependency.getSystemPath();
628                 if (sysPath != null && !sysPath.isEmpty()) {
629                     if (!hasExpression(sysPath)) {
630                         addViolation(
631                                 problems,
632                                 Severity.WARNING,
633                                 Version.V20,
634                                 prefix + prefix2 + "systemPath",
635                                 key,
636                                 "should use a variable instead of a hard-coded path " + sysPath,
637                                 dependency);
638                     } else if (sysPath.contains("${basedir}") || sysPath.contains("${project.basedir}")) {
639                         addViolation(
640                                 problems,
641                                 Severity.WARNING,
642                                 Version.V20,
643                                 prefix + prefix2 + "systemPath",
644                                 key,
645                                 "should not point at files within the project directory, " + sysPath
646                                         + " will be unresolvable by dependent projects",
647                                 dependency);
648                     }
649                 }
650             }
651 
652             if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) {
653                 addViolation(
654                         problems,
655                         Severity.WARNING,
656                         Version.BASE,
657                         prefix + prefix2 + "version",
658                         key,
659                         "is either LATEST or RELEASE (both of them are being deprecated)",
660                         dependency);
661             }
662 
663             Dependency existing = index.get(key);
664 
665             if (existing != null) {
666                 String msg;
667                 if (equals(existing.getVersion(), dependency.getVersion())) {
668                     msg = "duplicate declaration of version " + Objects.toString(dependency.getVersion(), "(?)");
669                 } else {
670                     msg = "version " + Objects.toString(existing.getVersion(), "(?)") + " vs "
671                             + Objects.toString(dependency.getVersion(), "(?)");
672                 }
673 
674                 addViolation(
675                         problems,
676                         errOn31,
677                         Version.V20,
678                         prefix + prefix2 + "(groupId:artifactId:type:classifier)",
679                         null,
680                         "must be unique: " + key + " -> " + msg,
681                         dependency);
682             } else {
683                 index.put(key, dependency);
684             }
685         }
686     }
687 
688     private void validate20RawDependenciesSelfReferencing(
689             ModelProblemCollector problems,
690             org.apache.maven.api.model.Model m,
691             List<Dependency> dependencies,
692             String prefix,
693             ModelBuildingRequest request) {
694         // We only check for groupId/artifactId/version/classifier cause if there is another
695         // module with the same groupId/artifactId/version/classifier this will fail the build
696         // earlier like "Project '...' is duplicated in the reactor.
697         // So it is sufficient to check only groupId/artifactId/version/classifier and not the
698         // packaging type.
699         for (Dependency dependency : dependencies) {
700             String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
701                     + (dependency.getClassifier() != null ? ":" + dependency.getClassifier() : EMPTY);
702             String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
703             if (key.equals(mKey)) {
704                 // This means a module which is build has a dependency which has the same
705                 // groupId, artifactId, version and classifier coordinates. This is in consequence
706                 // a self reference or in other words a circular reference which can not being resolved.
707                 addViolation(
708                         problems,
709                         Severity.FATAL,
710                         Version.V31,
711                         prefix + "[" + key + "]",
712                         key,
713                         "is referencing itself.",
714                         dependency);
715             }
716         }
717     }
718 
719     private void validateEffectiveDependencies(
720             ModelProblemCollector problems,
721             org.apache.maven.api.model.Model m,
722             List<Dependency> dependencies,
723             boolean management,
724             ModelBuildingRequest request) {
725         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
726 
727         String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";
728 
729         for (Dependency d : dependencies) {
730             validateEffectiveDependency(problems, d, management, prefix, request);
731 
732             if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
733                 validateBoolean(
734                         prefix, "optional", problems, errOn30, Version.V20, d.getOptional(), d.getManagementKey(), d);
735 
736                 if (!management) {
737                     validateVersion(
738                             prefix, "version", problems, errOn30, Version.V20, d.getVersion(), d.getManagementKey(), d);
739 
740                     /*
741                      * TODO Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In
742                      * order to don't break backward-compat with those, only warn but don't error out.
743                      */
744                     validateEnum(
745                             prefix,
746                             "scope",
747                             problems,
748                             Severity.WARNING,
749                             Version.V20,
750                             d.getScope(),
751                             d.getManagementKey(),
752                             d,
753                             "provided",
754                             "compile",
755                             "runtime",
756                             "test",
757                             "system");
758 
759                     validateEffectiveModelAgainstDependency(prefix, problems, m, d, request);
760                 } else {
761                     validateEnum(
762                             prefix,
763                             "scope",
764                             problems,
765                             Severity.WARNING,
766                             Version.V20,
767                             d.getScope(),
768                             d.getManagementKey(),
769                             d,
770                             "provided",
771                             "compile",
772                             "runtime",
773                             "test",
774                             "system",
775                             "import");
776                 }
777             }
778         }
779     }
780 
781     private void validateEffectiveModelAgainstDependency(
782             String prefix,
783             ModelProblemCollector problems,
784             org.apache.maven.api.model.Model m,
785             Dependency d,
786             ModelBuildingRequest request) {
787         String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion()
788                 + (d.getClassifier() != null ? ":" + d.getClassifier() : EMPTY);
789         String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
790         if (key.equals(mKey)) {
791             // This means a module which is build has a dependency which has the same
792             // groupId, artifactId, version and classifier coordinates. This is in consequence
793             // a self reference or in other words a circular reference which can not being resolved.
794             addViolation(
795                     problems, Severity.FATAL, Version.V31, prefix + "[" + key + "]", key, "is referencing itself.", d);
796         }
797     }
798 
799     private void validate20EffectivePluginDependencies(
800             ModelProblemCollector problems, Plugin plugin, ModelBuildingRequest request) {
801         List<Dependency> dependencies = plugin.getDependencies();
802 
803         if (!dependencies.isEmpty()) {
804             String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
805 
806             Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
807 
808             for (Dependency d : dependencies) {
809                 validateEffectiveDependency(problems, d, false, prefix, request);
810 
811                 validateVersion(
812                         prefix, "version", problems, errOn30, Version.BASE, d.getVersion(), d.getManagementKey(), d);
813 
814                 validateEnum(
815                         prefix,
816                         "scope",
817                         problems,
818                         errOn30,
819                         Version.BASE,
820                         d.getScope(),
821                         d.getManagementKey(),
822                         d,
823                         "compile",
824                         "runtime",
825                         "system");
826             }
827         }
828     }
829 
830     private void validateEffectiveDependency(
831             ModelProblemCollector problems,
832             Dependency d,
833             boolean management,
834             String prefix,
835             ModelBuildingRequest request) {
836         validateCoordinateId(
837                 prefix,
838                 "artifactId",
839                 problems,
840                 Severity.ERROR,
841                 Version.BASE,
842                 d.getArtifactId(),
843                 d.getManagementKey(),
844                 d);
845 
846         validateCoordinateId(
847                 prefix, "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(), d);
848 
849         if (!management) {
850             validateStringNotEmpty(
851                     prefix, "type", problems, Severity.ERROR, Version.BASE, d.getType(), d.getManagementKey(), d);
852 
853             validateDependencyVersion(problems, d, prefix);
854         }
855 
856         if ("system".equals(d.getScope())) {
857             String systemPath = d.getSystemPath();
858 
859             if (systemPath == null || systemPath.isEmpty()) {
860                 addViolation(
861                         problems,
862                         Severity.ERROR,
863                         Version.BASE,
864                         prefix + "systemPath",
865                         d.getManagementKey(),
866                         "is missing.",
867                         d);
868             } else {
869                 File sysFile = new File(systemPath);
870                 if (!sysFile.isAbsolute()) {
871                     addViolation(
872                             problems,
873                             Severity.ERROR,
874                             Version.BASE,
875                             prefix + "systemPath",
876                             d.getManagementKey(),
877                             "must specify an absolute path but is " + systemPath,
878                             d);
879                 } else if (!sysFile.isFile()) {
880                     String msg = "refers to a non-existing file " + sysFile.getAbsolutePath();
881                     systemPath = systemPath.replace('/', File.separatorChar).replace('\\', File.separatorChar);
882                     String jdkHome =
883                             request.getSystemProperties().getProperty("java.home", EMPTY) + File.separator + "..";
884                     if (systemPath.startsWith(jdkHome)) {
885                         msg += ". Please verify that you run Maven using a JDK and not just a JRE.";
886                     }
887                     addViolation(
888                             problems,
889                             Severity.WARNING,
890                             Version.BASE,
891                             prefix + "systemPath",
892                             d.getManagementKey(),
893                             msg,
894                             d);
895                 }
896             }
897         } else if (d.getSystemPath() != null && !d.getSystemPath().isEmpty()) {
898             addViolation(
899                     problems,
900                     Severity.ERROR,
901                     Version.BASE,
902                     prefix + "systemPath",
903                     d.getManagementKey(),
904                     "must be omitted. This field may only be specified for a dependency with system scope.",
905                     d);
906         }
907 
908         if (request.getValidationLevel() >= ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0) {
909             for (Exclusion exclusion : d.getExclusions()) {
910                 if (request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0) {
911                     validateCoordinateId(
912                             prefix,
913                             "exclusions.exclusion.groupId",
914                             problems,
915                             Severity.WARNING,
916                             Version.V20,
917                             exclusion.getGroupId(),
918                             d.getManagementKey(),
919                             exclusion);
920 
921                     validateCoordinateId(
922                             prefix,
923                             "exclusions.exclusion.artifactId",
924                             problems,
925                             Severity.WARNING,
926                             Version.V20,
927                             exclusion.getArtifactId(),
928                             d.getManagementKey(),
929                             exclusion);
930                 } else {
931                     validateCoordinateIdWithWildcards(
932                             prefix,
933                             "exclusions.exclusion.groupId",
934                             problems,
935                             Severity.WARNING,
936                             Version.V30,
937                             exclusion.getGroupId(),
938                             d.getManagementKey(),
939                             exclusion);
940 
941                     validateCoordinateIdWithWildcards(
942                             prefix,
943                             "exclusions.exclusion.artifactId",
944                             problems,
945                             Severity.WARNING,
946                             Version.V30,
947                             exclusion.getArtifactId(),
948                             d.getManagementKey(),
949                             exclusion);
950                 }
951             }
952         }
953     }
954 
955     /**
956      * @since 3.2.4
957      */
958     protected void validateDependencyVersion(ModelProblemCollector problems, Dependency d, String prefix) {
959         validateStringNotEmpty(
960                 prefix, "version", problems, Severity.ERROR, Version.BASE, d.getVersion(), d.getManagementKey(), d);
961     }
962 
963     private void validateRawRepositories(
964             ModelProblemCollector problems,
965             List<Repository> repositories,
966             String prefix,
967             String prefix2,
968             ModelBuildingRequest request) {
969         Map<String, Repository> index = new HashMap<>();
970 
971         for (Repository repository : repositories) {
972             validateStringNotEmpty(
973                     prefix, prefix2, "id", problems, Severity.ERROR, Version.V20, repository.getId(), null, repository);
974 
975             if (validateStringNotEmpty(
976                     prefix,
977                     prefix2,
978                     "[" + repository.getId() + "].url",
979                     problems,
980                     Severity.ERROR,
981                     Version.V20,
982                     repository.getUrl(),
983                     null,
984                     repository)) {
985                 // only allow ${basedir} and ${project.basedir}
986                 Matcher m = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
987                 while (m.find()) {
988                     if (!("basedir".equals(m.group(1)) || "project.basedir".equals(m.group(1)))) {
989                         validateStringNoExpression(
990                                 prefix + prefix2 + "[" + repository.getId() + "].url",
991                                 problems,
992                                 Severity.ERROR,
993                                 Version.V40,
994                                 repository.getUrl(),
995                                 repository);
996                         break;
997                     }
998                 }
999             }
1000 
1001             String key = repository.getId();
1002 
1003             Repository existing = index.get(key);
1004 
1005             if (existing != null) {
1006                 Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1007 
1008                 addViolation(
1009                         problems,
1010                         errOn30,
1011                         Version.V20,
1012                         prefix + prefix2 + "id",
1013                         null,
1014                         "must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs "
1015                                 + repository.getUrl(),
1016                         repository);
1017             } else {
1018                 index.put(key, repository);
1019             }
1020         }
1021     }
1022 
1023     private void validate20EffectiveRepository(
1024             ModelProblemCollector problems, Repository repository, String prefix, ModelBuildingRequest request) {
1025         if (repository != null) {
1026             Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);
1027 
1028             validateBannedCharacters(
1029                     prefix,
1030                     "id",
1031                     problems,
1032                     errOn31,
1033                     Version.V20,
1034                     repository.getId(),
1035                     null,
1036                     repository,
1037                     ILLEGAL_REPO_ID_CHARS);
1038 
1039             if ("local".equals(repository.getId())) {
1040                 addViolation(
1041                         problems,
1042                         errOn31,
1043                         Version.V20,
1044                         prefix + "id",
1045                         null,
1046                         "must not be 'local'" + ", this identifier is reserved for the local repository"
1047                                 + ", using it for other repositories will corrupt your repository metadata.",
1048                         repository);
1049             }
1050 
1051             if ("legacy".equals(repository.getLayout())) {
1052                 addViolation(
1053                         problems,
1054                         Severity.WARNING,
1055                         Version.V20,
1056                         prefix + "layout",
1057                         repository.getId(),
1058                         "uses the unsupported value 'legacy', artifact resolution might fail.",
1059                         repository);
1060             }
1061         }
1062     }
1063 
1064     private void validate20RawResources(
1065             ModelProblemCollector problems, List<Resource> resources, String prefix, ModelBuildingRequest request) {
1066         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1067 
1068         for (Resource resource : resources) {
1069             validateStringNotEmpty(
1070                     prefix,
1071                     "directory",
1072                     problems,
1073                     Severity.ERROR,
1074                     Version.V20,
1075                     resource.getDirectory(),
1076                     null,
1077                     resource);
1078 
1079             validateBoolean(
1080                     prefix,
1081                     "filtering",
1082                     problems,
1083                     errOn30,
1084                     Version.V20,
1085                     resource.getFiltering(),
1086                     resource.getDirectory(),
1087                     resource);
1088         }
1089     }
1090 
1091     // ----------------------------------------------------------------------
1092     // Field validation
1093     // ----------------------------------------------------------------------
1094 
1095     private boolean validateCoordinateId(
1096             String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) {
1097         return validateCoordinateId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
1098     }
1099 
1100     @SuppressWarnings("checkstyle:parameternumber")
1101     private boolean validateCoordinateId(
1102             String prefix,
1103             String fieldName,
1104             ModelProblemCollector problems,
1105             Severity severity,
1106             Version version,
1107             String id,
1108             String sourceHint,
1109             InputLocationTracker tracker) {
1110         if (validCoordinateIds.contains(id)) {
1111             return true;
1112         }
1113         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1114             return false;
1115         } else {
1116             if (!isValidCoordinateId(id)) {
1117                 addViolation(
1118                         problems,
1119                         severity,
1120                         version,
1121                         prefix + fieldName,
1122                         sourceHint,
1123                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1124                         tracker);
1125                 return false;
1126             }
1127             validCoordinateIds.add(id);
1128             return true;
1129         }
1130     }
1131 
1132     private boolean isValidCoordinateId(String id) {
1133         for (int i = 0; i < id.length(); i++) {
1134             char c = id.charAt(i);
1135             if (!isValidCoordinateIdCharacter(c)) {
1136                 return false;
1137             }
1138         }
1139         return true;
1140     }
1141 
1142     private boolean isValidCoordinateIdCharacter(char c) {
1143         return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.';
1144     }
1145 
1146     @SuppressWarnings("checkstyle:parameternumber")
1147     private boolean validateProfileId(
1148             String prefix,
1149             String fieldName,
1150             ModelProblemCollector problems,
1151             Severity severity,
1152             Version version,
1153             String id,
1154             String sourceHint,
1155             InputLocationTracker tracker) {
1156         if (validProfileIds.contains(id)) {
1157             return true;
1158         }
1159         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1160             return false;
1161         } else {
1162             if (!isValidProfileId(id)) {
1163                 addViolation(
1164                         problems,
1165                         severity,
1166                         version,
1167                         prefix + fieldName,
1168                         sourceHint,
1169                         "with value '" + id + "' does not match a valid profile id pattern.",
1170                         tracker);
1171                 return false;
1172             }
1173             validProfileIds.add(id);
1174             return true;
1175         }
1176     }
1177 
1178     private boolean isValidProfileId(String id) {
1179         switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx"
1180             case '+': // activate
1181             case '-': // deactivate
1182             case '!': // deactivate
1183             case '?': // optional
1184                 return false;
1185             default:
1186         }
1187         return true;
1188     }
1189 
1190     @SuppressWarnings("checkstyle:parameternumber")
1191     private boolean validateCoordinateIdWithWildcards(
1192             String prefix,
1193             String fieldName,
1194             ModelProblemCollector problems,
1195             Severity severity,
1196             Version version,
1197             String id,
1198             String sourceHint,
1199             InputLocationTracker tracker) {
1200         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1201             return false;
1202         } else {
1203             if (!isValidCoordinateIdWithWildCards(id)) {
1204                 addViolation(
1205                         problems,
1206                         severity,
1207                         version,
1208                         prefix + fieldName,
1209                         sourceHint,
1210                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1211                         tracker);
1212                 return false;
1213             }
1214             return true;
1215         }
1216     }
1217 
1218     private boolean isValidCoordinateIdWithWildCards(String id) {
1219         for (int i = 0; i < id.length(); i++) {
1220             char c = id.charAt(i);
1221             if (!isValidCoordinateIdWithWildCardCharacter(c)) {
1222                 return false;
1223             }
1224         }
1225         return true;
1226     }
1227 
1228     private boolean isValidCoordinateIdWithWildCardCharacter(char c) {
1229         return isValidCoordinateIdCharacter(c) || c == '?' || c == '*';
1230     }
1231 
1232     private boolean validateStringNoExpression(
1233             String fieldName,
1234             ModelProblemCollector problems,
1235             Severity severity,
1236             Version version,
1237             String string,
1238             InputLocationTracker tracker) {
1239         if (!hasExpression(string)) {
1240             return true;
1241         }
1242 
1243         addViolation(
1244                 problems,
1245                 severity,
1246                 version,
1247                 fieldName,
1248                 null,
1249                 "contains an expression but should be a constant.",
1250                 tracker);
1251 
1252         return false;
1253     }
1254 
1255     private boolean validateVersionNoExpression(
1256             String fieldName,
1257             ModelProblemCollector problems,
1258             Severity severity,
1259             Version version,
1260             String string,
1261             InputLocationTracker tracker) {
1262         if (!hasExpression(string)) {
1263             return true;
1264         }
1265 
1266         Matcher m = EXPRESSION_NAME_PATTERN.matcher(string.trim());
1267         while (m.find()) {
1268             String property = m.group(1);
1269             if (!versionProcessor.isValidProperty(property)) {
1270                 addViolation(
1271                         problems,
1272                         severity,
1273                         version,
1274                         fieldName,
1275                         null,
1276                         "contains an expression but should be a constant.",
1277                         tracker);
1278 
1279                 return false;
1280             }
1281         }
1282 
1283         return true;
1284     }
1285 
1286     private boolean hasExpression(String value) {
1287         return value != null && value.contains("${");
1288     }
1289 
1290     private boolean hasProjectExpression(String value) {
1291         return value != null && value.contains("${project.");
1292     }
1293 
1294     private boolean validateStringNotEmpty(
1295             String fieldName,
1296             ModelProblemCollector problems,
1297             Severity severity,
1298             Version version,
1299             String string,
1300             InputLocationTracker tracker) {
1301         return validateStringNotEmpty(EMPTY, fieldName, problems, severity, version, string, null, tracker);
1302     }
1303 
1304     /**
1305      * Asserts:
1306      * <p/>
1307      * <ul>
1308      * <li><code>string != null</code>
1309      * <li><code>string.length > 0</code>
1310      * </ul>
1311      */
1312     @SuppressWarnings("checkstyle:parameternumber")
1313     private boolean validateStringNotEmpty(
1314             String prefix,
1315             String prefix2,
1316             String fieldName,
1317             ModelProblemCollector problems,
1318             Severity severity,
1319             Version version,
1320             String string,
1321             String sourceHint,
1322             InputLocationTracker tracker) {
1323         if (!validateNotNull(prefix, prefix2, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1324             return false;
1325         }
1326 
1327         if (!string.isEmpty()) {
1328             return true;
1329         }
1330 
1331         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1332 
1333         return false;
1334     }
1335 
1336     /**
1337      * Asserts:
1338      * <p/>
1339      * <ul>
1340      * <li><code>string != null</code>
1341      * <li><code>string.length > 0</code>
1342      * </ul>
1343      */
1344     @SuppressWarnings("checkstyle:parameternumber")
1345     private boolean validateStringNotEmpty(
1346             String prefix,
1347             String fieldName,
1348             ModelProblemCollector problems,
1349             Severity severity,
1350             Version version,
1351             String string,
1352             String sourceHint,
1353             InputLocationTracker tracker) {
1354         if (!validateNotNull(prefix, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1355             return false;
1356         }
1357 
1358         if (string.length() > 0) {
1359             return true;
1360         }
1361 
1362         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1363 
1364         return false;
1365     }
1366 
1367     /**
1368      * Asserts:
1369      * <p/>
1370      * <ul>
1371      * <li><code>string != null</code>
1372      * </ul>
1373      */
1374     @SuppressWarnings("checkstyle:parameternumber")
1375     private boolean validateNotNull(
1376             String prefix,
1377             String fieldName,
1378             ModelProblemCollector problems,
1379             Severity severity,
1380             Version version,
1381             Object object,
1382             String sourceHint,
1383             InputLocationTracker tracker) {
1384         if (object != null) {
1385             return true;
1386         }
1387 
1388         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1389 
1390         return false;
1391     }
1392 
1393     /**
1394      * Asserts:
1395      * <p/>
1396      * <ul>
1397      * <li><code>string != null</code>
1398      * </ul>
1399      */
1400     @SuppressWarnings("checkstyle:parameternumber")
1401     private boolean validateNotNull(
1402             String prefix,
1403             String prefix2,
1404             String fieldName,
1405             ModelProblemCollector problems,
1406             Severity severity,
1407             Version version,
1408             Object object,
1409             String sourceHint,
1410             InputLocationTracker tracker) {
1411         if (object != null) {
1412             return true;
1413         }
1414 
1415         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1416 
1417         return false;
1418     }
1419 
1420     @SuppressWarnings("checkstyle:parameternumber")
1421     private boolean validateBoolean(
1422             String prefix,
1423             String fieldName,
1424             ModelProblemCollector problems,
1425             Severity severity,
1426             Version version,
1427             String string,
1428             String sourceHint,
1429             InputLocationTracker tracker) {
1430         if (string == null || string.length() <= 0) {
1431             return true;
1432         }
1433 
1434         if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
1435             return true;
1436         }
1437 
1438         addViolation(
1439                 problems,
1440                 severity,
1441                 version,
1442                 prefix + fieldName,
1443                 sourceHint,
1444                 "must be 'true' or 'false' but is '" + string + "'.",
1445                 tracker);
1446 
1447         return false;
1448     }
1449 
1450     @SuppressWarnings("checkstyle:parameternumber")
1451     private boolean validateEnum(
1452             String prefix,
1453             String fieldName,
1454             ModelProblemCollector problems,
1455             Severity severity,
1456             Version version,
1457             String string,
1458             String sourceHint,
1459             InputLocationTracker tracker,
1460             String... validValues) {
1461         if (string == null || string.length() <= 0) {
1462             return true;
1463         }
1464 
1465         List<String> values = Arrays.asList(validValues);
1466 
1467         if (values.contains(string)) {
1468             return true;
1469         }
1470 
1471         addViolation(
1472                 problems,
1473                 severity,
1474                 version,
1475                 prefix + fieldName,
1476                 sourceHint,
1477                 "must be one of " + values + " but is '" + string + "'.",
1478                 tracker);
1479 
1480         return false;
1481     }
1482 
1483     @SuppressWarnings("checkstyle:parameternumber")
1484     private boolean validateModelVersion(
1485             ModelProblemCollector problems, String string, InputLocationTracker tracker, List<String> validVersions) {
1486         if (string == null || string.length() <= 0) {
1487             return true;
1488         }
1489 
1490         if (validVersions.contains(string)) {
1491             return true;
1492         }
1493 
1494         boolean newerThanAll = true;
1495         boolean olderThanAll = true;
1496         for (String validValue : validVersions) {
1497             final int comparison = compareModelVersions(validValue, string);
1498             newerThanAll = newerThanAll && comparison < 0;
1499             olderThanAll = olderThanAll && comparison > 0;
1500         }
1501 
1502         if (newerThanAll) {
1503             addViolation(
1504                     problems,
1505                     Severity.FATAL,
1506                     Version.V20,
1507                     "modelVersion",
1508                     null,
1509                     "of '" + string + "' is newer than the versions supported by this version of Maven: "
1510                             + validVersions + ". Building this project requires a newer version of Maven.",
1511                     tracker);
1512 
1513         } else if (olderThanAll) {
1514             // note this will not be hit for Maven 1.x project.xml as it is an incompatible schema
1515             addViolation(
1516                     problems,
1517                     Severity.FATAL,
1518                     Version.V20,
1519                     "modelVersion",
1520                     null,
1521                     "of '" + string + "' is older than the versions supported by this version of Maven: "
1522                             + validVersions + ". Building this project requires an older version of Maven.",
1523                     tracker);
1524 
1525         } else {
1526             addViolation(
1527                     problems,
1528                     Severity.ERROR,
1529                     Version.V20,
1530                     "modelVersion",
1531                     null,
1532                     "must be one of " + validVersions + " but is '" + string + "'.",
1533                     tracker);
1534         }
1535 
1536         return false;
1537     }
1538 
1539     /**
1540      * Compares two model versions.
1541      *
1542      * @param first the first version.
1543      * @param second the second version.
1544      * @return negative if the first version is newer than the second version, zero if they are the same or positive if
1545      * the second version is the newer.
1546      */
1547     private static int compareModelVersions(String first, String second) {
1548         // we use a dedicated comparator because we control our model version scheme.
1549         String[] firstSegments = first.split("\\.");
1550         String[] secondSegments = second.split("\\.");
1551         for (int i = 0; i < Math.max(firstSegments.length, secondSegments.length); i++) {
1552             int result = Long.valueOf(i < firstSegments.length ? firstSegments[i] : "0")
1553                     .compareTo(Long.valueOf(i < secondSegments.length ? secondSegments[i] : "0"));
1554             if (result != 0) {
1555                 return result;
1556             }
1557         }
1558         return 0;
1559     }
1560 
1561     @SuppressWarnings("checkstyle:parameternumber")
1562     private boolean validateBannedCharacters(
1563             String prefix,
1564             String fieldName,
1565             ModelProblemCollector problems,
1566             Severity severity,
1567             Version version,
1568             String string,
1569             String sourceHint,
1570             InputLocationTracker tracker,
1571             String banned) {
1572         if (string != null) {
1573             for (int i = string.length() - 1; i >= 0; i--) {
1574                 if (banned.indexOf(string.charAt(i)) >= 0) {
1575                     addViolation(
1576                             problems,
1577                             severity,
1578                             version,
1579                             prefix + fieldName,
1580                             sourceHint,
1581                             "must not contain any of these characters " + banned + " but found " + string.charAt(i),
1582                             tracker);
1583                     return false;
1584                 }
1585             }
1586         }
1587 
1588         return true;
1589     }
1590 
1591     @SuppressWarnings("checkstyle:parameternumber")
1592     private boolean validateVersion(
1593             String prefix,
1594             String fieldName,
1595             ModelProblemCollector problems,
1596             Severity severity,
1597             Version version,
1598             String string,
1599             String sourceHint,
1600             InputLocationTracker tracker) {
1601         if (string == null || string.length() <= 0) {
1602             return true;
1603         }
1604 
1605         if (hasExpression(string)) {
1606             addViolation(
1607                     problems,
1608                     severity,
1609                     version,
1610                     prefix + fieldName,
1611                     sourceHint,
1612                     "must be a valid version but is '" + string + "'.",
1613                     tracker);
1614             return false;
1615         }
1616 
1617         return validateBannedCharacters(
1618                 prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
1619     }
1620 
1621     private boolean validate20ProperSnapshotVersion(
1622             String fieldName,
1623             ModelProblemCollector problems,
1624             Severity severity,
1625             Version version,
1626             String string,
1627             String sourceHint,
1628             InputLocationTracker tracker) {
1629         if (string == null || string.length() <= 0) {
1630             return true;
1631         }
1632 
1633         if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
1634             addViolation(
1635                     problems,
1636                     severity,
1637                     version,
1638                     fieldName,
1639                     sourceHint,
1640                     "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.",
1641                     tracker);
1642             return false;
1643         }
1644 
1645         return true;
1646     }
1647 
1648     private boolean validate20PluginVersion(
1649             String fieldName,
1650             ModelProblemCollector problems,
1651             String string,
1652             String sourceHint,
1653             InputLocationTracker tracker,
1654             ModelBuildingRequest request) {
1655         if (string == null) {
1656             // NOTE: The check for missing plugin versions is handled directly by the model builder
1657             return true;
1658         }
1659 
1660         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1661 
1662         if (!validateVersion(EMPTY, fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
1663             return false;
1664         }
1665 
1666         if (string.length() <= 0 || "RELEASE".equals(string) || "LATEST".equals(string)) {
1667             addViolation(
1668                     problems,
1669                     errOn30,
1670                     Version.V20,
1671                     fieldName,
1672                     sourceHint,
1673                     "must be a valid version but is '" + string + "'.",
1674                     tracker);
1675             return false;
1676         }
1677 
1678         return true;
1679     }
1680 
1681     private static void addViolation(
1682             ModelProblemCollector problems,
1683             Severity severity,
1684             Version version,
1685             String fieldName,
1686             String sourceHint,
1687             String message,
1688             InputLocationTracker tracker) {
1689         StringBuilder buffer = new StringBuilder(256);
1690         buffer.append('\'').append(fieldName).append('\'');
1691 
1692         if (sourceHint != null) {
1693             buffer.append(" for ").append(sourceHint);
1694         }
1695 
1696         buffer.append(' ').append(message);
1697 
1698         problems.add(new ModelProblemCollectorRequest(severity, version)
1699                 .setMessage(buffer.toString())
1700                 .setLocation(getLocation(fieldName, tracker)));
1701     }
1702 
1703     private static org.apache.maven.model.InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
1704         InputLocation location = null;
1705 
1706         if (tracker != null) {
1707             if (fieldName != null) {
1708                 Object key = fieldName;
1709 
1710                 int idx = fieldName.lastIndexOf('.');
1711                 if (idx >= 0) {
1712                     fieldName = fieldName.substring(idx + 1);
1713                     key = fieldName;
1714                 }
1715 
1716                 if (fieldName.endsWith("]")) {
1717                     key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
1718                     try {
1719                         key = Integer.valueOf(key.toString());
1720                     } catch (NumberFormatException e) {
1721                         // use key as is
1722                     }
1723                 }
1724 
1725                 location = tracker.getLocation(key);
1726             }
1727 
1728             if (location == null) {
1729                 location = tracker.getLocation(EMPTY);
1730             }
1731         }
1732 
1733         return location != null ? new org.apache.maven.model.InputLocation(location) : null;
1734     }
1735 
1736     private static boolean equals(String s1, String s2) {
1737         String c1 = s1 == null ? "" : s1.trim();
1738         String c2 = s2 == null ? "" : s2.trim();
1739         return c1.equals(c2);
1740     }
1741 
1742     private static Severity getSeverity(ModelBuildingRequest request, int errorThreshold) {
1743         return getSeverity(request.getValidationLevel(), errorThreshold);
1744     }
1745 
1746     private static Severity getSeverity(int validationLevel, int errorThreshold) {
1747         if (validationLevel < errorThreshold) {
1748             return Severity.WARNING;
1749         } else {
1750             return Severity.ERROR;
1751         }
1752     }
1753 }