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