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