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}, ${project.basedir} or ${project.baseUri}
986                 Matcher m = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
987                 while (m.find()) {
988                     if (!("basedir".equals(m.group(1))
989                             || "project.basedir".equals(m.group(1))
990                             || "project.baseUri".equals(m.group(1)))) {
991                         validateStringNoExpression(
992                                 prefix + prefix2 + "[" + repository.getId() + "].url",
993                                 problems,
994                                 Severity.ERROR,
995                                 Version.V40,
996                                 repository.getUrl(),
997                                 repository);
998                         break;
999                     }
1000                 }
1001             }
1002 
1003             String key = repository.getId();
1004 
1005             Repository existing = index.get(key);
1006 
1007             if (existing != null) {
1008                 Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1009 
1010                 addViolation(
1011                         problems,
1012                         errOn30,
1013                         Version.V20,
1014                         prefix + prefix2 + "id",
1015                         null,
1016                         "must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs "
1017                                 + repository.getUrl(),
1018                         repository);
1019             } else {
1020                 index.put(key, repository);
1021             }
1022         }
1023     }
1024 
1025     private void validate20EffectiveRepository(
1026             ModelProblemCollector problems, Repository repository, String prefix, ModelBuildingRequest request) {
1027         if (repository != null) {
1028             Severity errOn31 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_1);
1029 
1030             validateBannedCharacters(
1031                     prefix,
1032                     "id",
1033                     problems,
1034                     errOn31,
1035                     Version.V20,
1036                     repository.getId(),
1037                     null,
1038                     repository,
1039                     ILLEGAL_REPO_ID_CHARS);
1040 
1041             if ("local".equals(repository.getId())) {
1042                 addViolation(
1043                         problems,
1044                         errOn31,
1045                         Version.V20,
1046                         prefix + "id",
1047                         null,
1048                         "must not be 'local'" + ", this identifier is reserved for the local repository"
1049                                 + ", using it for other repositories will corrupt your repository metadata.",
1050                         repository);
1051             }
1052 
1053             if ("legacy".equals(repository.getLayout())) {
1054                 addViolation(
1055                         problems,
1056                         Severity.WARNING,
1057                         Version.V20,
1058                         prefix + "layout",
1059                         repository.getId(),
1060                         "uses the unsupported value 'legacy', artifact resolution might fail.",
1061                         repository);
1062             }
1063         }
1064     }
1065 
1066     private void validate20RawResources(
1067             ModelProblemCollector problems, List<Resource> resources, String prefix, ModelBuildingRequest request) {
1068         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1069 
1070         for (Resource resource : resources) {
1071             validateStringNotEmpty(
1072                     prefix,
1073                     "directory",
1074                     problems,
1075                     Severity.ERROR,
1076                     Version.V20,
1077                     resource.getDirectory(),
1078                     null,
1079                     resource);
1080 
1081             validateBoolean(
1082                     prefix,
1083                     "filtering",
1084                     problems,
1085                     errOn30,
1086                     Version.V20,
1087                     resource.getFiltering(),
1088                     resource.getDirectory(),
1089                     resource);
1090         }
1091     }
1092 
1093     // ----------------------------------------------------------------------
1094     // Field validation
1095     // ----------------------------------------------------------------------
1096 
1097     private boolean validateCoordinateId(
1098             String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) {
1099         return validateCoordinateId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
1100     }
1101 
1102     @SuppressWarnings("checkstyle:parameternumber")
1103     private boolean validateCoordinateId(
1104             String prefix,
1105             String fieldName,
1106             ModelProblemCollector problems,
1107             Severity severity,
1108             Version version,
1109             String id,
1110             String sourceHint,
1111             InputLocationTracker tracker) {
1112         if (validCoordinateIds.contains(id)) {
1113             return true;
1114         }
1115         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1116             return false;
1117         } else {
1118             if (!isValidCoordinateId(id)) {
1119                 addViolation(
1120                         problems,
1121                         severity,
1122                         version,
1123                         prefix + fieldName,
1124                         sourceHint,
1125                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1126                         tracker);
1127                 return false;
1128             }
1129             validCoordinateIds.add(id);
1130             return true;
1131         }
1132     }
1133 
1134     private boolean isValidCoordinateId(String id) {
1135         for (int i = 0; i < id.length(); i++) {
1136             char c = id.charAt(i);
1137             if (!isValidCoordinateIdCharacter(c)) {
1138                 return false;
1139             }
1140         }
1141         return true;
1142     }
1143 
1144     private boolean isValidCoordinateIdCharacter(char c) {
1145         return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.';
1146     }
1147 
1148     @SuppressWarnings("checkstyle:parameternumber")
1149     private boolean validateProfileId(
1150             String prefix,
1151             String fieldName,
1152             ModelProblemCollector problems,
1153             Severity severity,
1154             Version version,
1155             String id,
1156             String sourceHint,
1157             InputLocationTracker tracker) {
1158         if (validProfileIds.contains(id)) {
1159             return true;
1160         }
1161         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1162             return false;
1163         } else {
1164             if (!isValidProfileId(id)) {
1165                 addViolation(
1166                         problems,
1167                         severity,
1168                         version,
1169                         prefix + fieldName,
1170                         sourceHint,
1171                         "with value '" + id + "' does not match a valid profile id pattern.",
1172                         tracker);
1173                 return false;
1174             }
1175             validProfileIds.add(id);
1176             return true;
1177         }
1178     }
1179 
1180     private boolean isValidProfileId(String id) {
1181         switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx"
1182             case '+': // activate
1183             case '-': // deactivate
1184             case '!': // deactivate
1185             case '?': // optional
1186                 return false;
1187             default:
1188         }
1189         return true;
1190     }
1191 
1192     @SuppressWarnings("checkstyle:parameternumber")
1193     private boolean validateCoordinateIdWithWildcards(
1194             String prefix,
1195             String fieldName,
1196             ModelProblemCollector problems,
1197             Severity severity,
1198             Version version,
1199             String id,
1200             String sourceHint,
1201             InputLocationTracker tracker) {
1202         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1203             return false;
1204         } else {
1205             if (!isValidCoordinateIdWithWildCards(id)) {
1206                 addViolation(
1207                         problems,
1208                         severity,
1209                         version,
1210                         prefix + fieldName,
1211                         sourceHint,
1212                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1213                         tracker);
1214                 return false;
1215             }
1216             return true;
1217         }
1218     }
1219 
1220     private boolean isValidCoordinateIdWithWildCards(String id) {
1221         for (int i = 0; i < id.length(); i++) {
1222             char c = id.charAt(i);
1223             if (!isValidCoordinateIdWithWildCardCharacter(c)) {
1224                 return false;
1225             }
1226         }
1227         return true;
1228     }
1229 
1230     private boolean isValidCoordinateIdWithWildCardCharacter(char c) {
1231         return isValidCoordinateIdCharacter(c) || c == '?' || c == '*';
1232     }
1233 
1234     private boolean validateStringNoExpression(
1235             String fieldName,
1236             ModelProblemCollector problems,
1237             Severity severity,
1238             Version version,
1239             String string,
1240             InputLocationTracker tracker) {
1241         if (!hasExpression(string)) {
1242             return true;
1243         }
1244 
1245         addViolation(
1246                 problems,
1247                 severity,
1248                 version,
1249                 fieldName,
1250                 null,
1251                 "contains an expression but should be a constant.",
1252                 tracker);
1253 
1254         return false;
1255     }
1256 
1257     private boolean validateVersionNoExpression(
1258             String fieldName,
1259             ModelProblemCollector problems,
1260             Severity severity,
1261             Version version,
1262             String string,
1263             InputLocationTracker tracker) {
1264         if (!hasExpression(string)) {
1265             return true;
1266         }
1267 
1268         Matcher m = EXPRESSION_NAME_PATTERN.matcher(string.trim());
1269         while (m.find()) {
1270             String property = m.group(1);
1271             if (!versionProcessor.isValidProperty(property)) {
1272                 addViolation(
1273                         problems,
1274                         severity,
1275                         version,
1276                         fieldName,
1277                         null,
1278                         "contains an expression but should be a constant.",
1279                         tracker);
1280 
1281                 return false;
1282             }
1283         }
1284 
1285         return true;
1286     }
1287 
1288     private boolean hasExpression(String value) {
1289         return value != null && value.contains("${");
1290     }
1291 
1292     private boolean hasProjectExpression(String value) {
1293         return value != null && value.contains("${project.");
1294     }
1295 
1296     private boolean validateStringNotEmpty(
1297             String fieldName,
1298             ModelProblemCollector problems,
1299             Severity severity,
1300             Version version,
1301             String string,
1302             InputLocationTracker tracker) {
1303         return validateStringNotEmpty(EMPTY, fieldName, problems, severity, version, string, null, tracker);
1304     }
1305 
1306     /**
1307      * Asserts:
1308      * <p/>
1309      * <ul>
1310      * <li><code>string != null</code>
1311      * <li><code>string.length > 0</code>
1312      * </ul>
1313      */
1314     @SuppressWarnings("checkstyle:parameternumber")
1315     private boolean validateStringNotEmpty(
1316             String prefix,
1317             String prefix2,
1318             String fieldName,
1319             ModelProblemCollector problems,
1320             Severity severity,
1321             Version version,
1322             String string,
1323             String sourceHint,
1324             InputLocationTracker tracker) {
1325         if (!validateNotNull(prefix, prefix2, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1326             return false;
1327         }
1328 
1329         if (!string.isEmpty()) {
1330             return true;
1331         }
1332 
1333         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1334 
1335         return false;
1336     }
1337 
1338     /**
1339      * Asserts:
1340      * <p/>
1341      * <ul>
1342      * <li><code>string != null</code>
1343      * <li><code>string.length > 0</code>
1344      * </ul>
1345      */
1346     @SuppressWarnings("checkstyle:parameternumber")
1347     private boolean validateStringNotEmpty(
1348             String prefix,
1349             String fieldName,
1350             ModelProblemCollector problems,
1351             Severity severity,
1352             Version version,
1353             String string,
1354             String sourceHint,
1355             InputLocationTracker tracker) {
1356         if (!validateNotNull(prefix, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1357             return false;
1358         }
1359 
1360         if (!string.isEmpty()) {
1361             return true;
1362         }
1363 
1364         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1365 
1366         return false;
1367     }
1368 
1369     /**
1370      * Asserts:
1371      * <p/>
1372      * <ul>
1373      * <li><code>string != null</code>
1374      * </ul>
1375      */
1376     @SuppressWarnings("checkstyle:parameternumber")
1377     private boolean validateNotNull(
1378             String prefix,
1379             String fieldName,
1380             ModelProblemCollector problems,
1381             Severity severity,
1382             Version version,
1383             Object object,
1384             String sourceHint,
1385             InputLocationTracker tracker) {
1386         if (object != null) {
1387             return true;
1388         }
1389 
1390         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1391 
1392         return false;
1393     }
1394 
1395     /**
1396      * Asserts:
1397      * <p/>
1398      * <ul>
1399      * <li><code>string != null</code>
1400      * </ul>
1401      */
1402     @SuppressWarnings("checkstyle:parameternumber")
1403     private boolean validateNotNull(
1404             String prefix,
1405             String prefix2,
1406             String fieldName,
1407             ModelProblemCollector problems,
1408             Severity severity,
1409             Version version,
1410             Object object,
1411             String sourceHint,
1412             InputLocationTracker tracker) {
1413         if (object != null) {
1414             return true;
1415         }
1416 
1417         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1418 
1419         return false;
1420     }
1421 
1422     @SuppressWarnings("checkstyle:parameternumber")
1423     private boolean validateBoolean(
1424             String prefix,
1425             String fieldName,
1426             ModelProblemCollector problems,
1427             Severity severity,
1428             Version version,
1429             String string,
1430             String sourceHint,
1431             InputLocationTracker tracker) {
1432         if (string == null || string.isEmpty()) {
1433             return true;
1434         }
1435 
1436         if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
1437             return true;
1438         }
1439 
1440         addViolation(
1441                 problems,
1442                 severity,
1443                 version,
1444                 prefix + fieldName,
1445                 sourceHint,
1446                 "must be 'true' or 'false' but is '" + string + "'.",
1447                 tracker);
1448 
1449         return false;
1450     }
1451 
1452     @SuppressWarnings("checkstyle:parameternumber")
1453     private boolean validateEnum(
1454             String prefix,
1455             String fieldName,
1456             ModelProblemCollector problems,
1457             Severity severity,
1458             Version version,
1459             String string,
1460             String sourceHint,
1461             InputLocationTracker tracker,
1462             String... validValues) {
1463         if (string == null || string.isEmpty()) {
1464             return true;
1465         }
1466 
1467         List<String> values = Arrays.asList(validValues);
1468 
1469         if (values.contains(string)) {
1470             return true;
1471         }
1472 
1473         addViolation(
1474                 problems,
1475                 severity,
1476                 version,
1477                 prefix + fieldName,
1478                 sourceHint,
1479                 "must be one of " + values + " but is '" + string + "'.",
1480                 tracker);
1481 
1482         return false;
1483     }
1484 
1485     @SuppressWarnings("checkstyle:parameternumber")
1486     private boolean validateModelVersion(
1487             ModelProblemCollector problems, String string, InputLocationTracker tracker, List<String> validVersions) {
1488         if (string == null || string.isEmpty()) {
1489             return true;
1490         }
1491 
1492         if (validVersions.contains(string)) {
1493             return true;
1494         }
1495 
1496         boolean newerThanAll = true;
1497         boolean olderThanAll = true;
1498         for (String validValue : validVersions) {
1499             final int comparison = compareModelVersions(validValue, string);
1500             newerThanAll = newerThanAll && comparison < 0;
1501             olderThanAll = olderThanAll && comparison > 0;
1502         }
1503 
1504         if (newerThanAll) {
1505             addViolation(
1506                     problems,
1507                     Severity.FATAL,
1508                     Version.V20,
1509                     "modelVersion",
1510                     null,
1511                     "of '" + string + "' is newer than the versions supported by this version of Maven: "
1512                             + validVersions + ". Building this project requires a newer version of Maven.",
1513                     tracker);
1514 
1515         } else if (olderThanAll) {
1516             // note this will not be hit for Maven 1.x project.xml as it is an incompatible schema
1517             addViolation(
1518                     problems,
1519                     Severity.FATAL,
1520                     Version.V20,
1521                     "modelVersion",
1522                     null,
1523                     "of '" + string + "' is older than the versions supported by this version of Maven: "
1524                             + validVersions + ". Building this project requires an older version of Maven.",
1525                     tracker);
1526 
1527         } else {
1528             addViolation(
1529                     problems,
1530                     Severity.ERROR,
1531                     Version.V20,
1532                     "modelVersion",
1533                     null,
1534                     "must be one of " + validVersions + " but is '" + string + "'.",
1535                     tracker);
1536         }
1537 
1538         return false;
1539     }
1540 
1541     /**
1542      * Compares two model versions.
1543      *
1544      * @param first the first version.
1545      * @param second the second version.
1546      * @return negative if the first version is newer than the second version, zero if they are the same or positive if
1547      * the second version is the newer.
1548      */
1549     private static int compareModelVersions(String first, String second) {
1550         // we use a dedicated comparator because we control our model version scheme.
1551         String[] firstSegments = first.split("\\.");
1552         String[] secondSegments = second.split("\\.");
1553         for (int i = 0; i < Math.max(firstSegments.length, secondSegments.length); i++) {
1554             int result = Long.valueOf(i < firstSegments.length ? firstSegments[i] : "0")
1555                     .compareTo(Long.valueOf(i < secondSegments.length ? secondSegments[i] : "0"));
1556             if (result != 0) {
1557                 return result;
1558             }
1559         }
1560         return 0;
1561     }
1562 
1563     @SuppressWarnings("checkstyle:parameternumber")
1564     private boolean validateBannedCharacters(
1565             String prefix,
1566             String fieldName,
1567             ModelProblemCollector problems,
1568             Severity severity,
1569             Version version,
1570             String string,
1571             String sourceHint,
1572             InputLocationTracker tracker,
1573             String banned) {
1574         if (string != null) {
1575             for (int i = string.length() - 1; i >= 0; i--) {
1576                 if (banned.indexOf(string.charAt(i)) >= 0) {
1577                     addViolation(
1578                             problems,
1579                             severity,
1580                             version,
1581                             prefix + fieldName,
1582                             sourceHint,
1583                             "must not contain any of these characters " + banned + " but found " + string.charAt(i),
1584                             tracker);
1585                     return false;
1586                 }
1587             }
1588         }
1589 
1590         return true;
1591     }
1592 
1593     @SuppressWarnings("checkstyle:parameternumber")
1594     private boolean validateVersion(
1595             String prefix,
1596             String fieldName,
1597             ModelProblemCollector problems,
1598             Severity severity,
1599             Version version,
1600             String string,
1601             String sourceHint,
1602             InputLocationTracker tracker) {
1603         if (string == null || string.isEmpty()) {
1604             return true;
1605         }
1606 
1607         if (hasExpression(string)) {
1608             addViolation(
1609                     problems,
1610                     severity,
1611                     version,
1612                     prefix + fieldName,
1613                     sourceHint,
1614                     "must be a valid version but is '" + string + "'.",
1615                     tracker);
1616             return false;
1617         }
1618 
1619         return validateBannedCharacters(
1620                 prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
1621     }
1622 
1623     private boolean validate20ProperSnapshotVersion(
1624             String fieldName,
1625             ModelProblemCollector problems,
1626             Severity severity,
1627             Version version,
1628             String string,
1629             String sourceHint,
1630             InputLocationTracker tracker) {
1631         if (string == null || string.isEmpty()) {
1632             return true;
1633         }
1634 
1635         if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
1636             addViolation(
1637                     problems,
1638                     severity,
1639                     version,
1640                     fieldName,
1641                     sourceHint,
1642                     "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.",
1643                     tracker);
1644             return false;
1645         }
1646 
1647         return true;
1648     }
1649 
1650     private boolean validate20PluginVersion(
1651             String fieldName,
1652             ModelProblemCollector problems,
1653             String string,
1654             String sourceHint,
1655             InputLocationTracker tracker,
1656             ModelBuildingRequest request) {
1657         if (string == null) {
1658             // NOTE: The check for missing plugin versions is handled directly by the model builder
1659             return true;
1660         }
1661 
1662         Severity errOn30 = getSeverity(request, ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_3_0);
1663 
1664         if (!validateVersion(EMPTY, fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
1665             return false;
1666         }
1667 
1668         if (string.isEmpty() || "RELEASE".equals(string) || "LATEST".equals(string)) {
1669             addViolation(
1670                     problems,
1671                     errOn30,
1672                     Version.V20,
1673                     fieldName,
1674                     sourceHint,
1675                     "must be a valid version but is '" + string + "'.",
1676                     tracker);
1677             return false;
1678         }
1679 
1680         return true;
1681     }
1682 
1683     private static void addViolation(
1684             ModelProblemCollector problems,
1685             Severity severity,
1686             Version version,
1687             String fieldName,
1688             String sourceHint,
1689             String message,
1690             InputLocationTracker tracker) {
1691         StringBuilder buffer = new StringBuilder(256);
1692         buffer.append('\'').append(fieldName).append('\'');
1693 
1694         if (sourceHint != null) {
1695             buffer.append(" for ").append(sourceHint);
1696         }
1697 
1698         buffer.append(' ').append(message);
1699 
1700         problems.add(new ModelProblemCollectorRequest(severity, version)
1701                 .setMessage(buffer.toString())
1702                 .setLocation(getLocation(fieldName, tracker)));
1703     }
1704 
1705     private static org.apache.maven.model.InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
1706         InputLocation location = null;
1707 
1708         if (tracker != null) {
1709             if (fieldName != null) {
1710                 Object key = fieldName;
1711 
1712                 int idx = fieldName.lastIndexOf('.');
1713                 if (idx >= 0) {
1714                     fieldName = fieldName.substring(idx + 1);
1715                     key = fieldName;
1716                 }
1717 
1718                 if (fieldName.endsWith("]")) {
1719                     key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
1720                     try {
1721                         key = Integer.valueOf(key.toString());
1722                     } catch (NumberFormatException e) {
1723                         // use key as is
1724                     }
1725                 }
1726 
1727                 location = tracker.getLocation(key);
1728             }
1729 
1730             if (location == null) {
1731                 location = tracker.getLocation(EMPTY);
1732             }
1733         }
1734 
1735         return location != null ? new org.apache.maven.model.InputLocation(location) : null;
1736     }
1737 
1738     private static boolean equals(String s1, String s2) {
1739         String c1 = s1 == null ? "" : s1.trim();
1740         String c2 = s2 == null ? "" : s2.trim();
1741         return c1.equals(c2);
1742     }
1743 
1744     private static Severity getSeverity(ModelBuildingRequest request, int errorThreshold) {
1745         return getSeverity(request.getValidationLevel(), errorThreshold);
1746     }
1747 
1748     private static Severity getSeverity(int validationLevel, int errorThreshold) {
1749         if (validationLevel < errorThreshold) {
1750             return Severity.WARNING;
1751         } else {
1752             return Severity.ERROR;
1753         }
1754     }
1755 }