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