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