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.impl.model;
20  
21  import java.io.File;
22  import java.util.Arrays;
23  import java.util.Deque;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Optional;
32  import java.util.Set;
33  import java.util.concurrent.ConcurrentHashMap;
34  import java.util.function.Function;
35  import java.util.function.Supplier;
36  import java.util.function.UnaryOperator;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  import java.util.stream.Collectors;
40  import java.util.stream.StreamSupport;
41  
42  import org.apache.maven.api.Session;
43  import org.apache.maven.api.annotations.Nullable;
44  import org.apache.maven.api.di.Inject;
45  import org.apache.maven.api.di.Named;
46  import org.apache.maven.api.di.Singleton;
47  import org.apache.maven.api.model.Activation;
48  import org.apache.maven.api.model.ActivationFile;
49  import org.apache.maven.api.model.ActivationOS;
50  import org.apache.maven.api.model.ActivationProperty;
51  import org.apache.maven.api.model.Build;
52  import org.apache.maven.api.model.BuildBase;
53  import org.apache.maven.api.model.Dependency;
54  import org.apache.maven.api.model.DependencyManagement;
55  import org.apache.maven.api.model.DistributionManagement;
56  import org.apache.maven.api.model.Exclusion;
57  import org.apache.maven.api.model.InputLocation;
58  import org.apache.maven.api.model.InputLocationTracker;
59  import org.apache.maven.api.model.Model;
60  import org.apache.maven.api.model.Parent;
61  import org.apache.maven.api.model.Plugin;
62  import org.apache.maven.api.model.PluginExecution;
63  import org.apache.maven.api.model.PluginManagement;
64  import org.apache.maven.api.model.Profile;
65  import org.apache.maven.api.model.ReportPlugin;
66  import org.apache.maven.api.model.Reporting;
67  import org.apache.maven.api.model.Repository;
68  import org.apache.maven.api.model.Resource;
69  import org.apache.maven.api.services.BuilderProblem.Severity;
70  import org.apache.maven.api.services.ModelBuilder;
71  import org.apache.maven.api.services.ModelProblem;
72  import org.apache.maven.api.services.ModelProblem.Version;
73  import org.apache.maven.api.services.ModelProblemCollector;
74  import org.apache.maven.api.services.model.ModelValidator;
75  import org.apache.maven.api.xml.XmlNode;
76  import org.apache.maven.api.xml.XmlService;
77  import org.apache.maven.impl.InternalSession;
78  import org.apache.maven.model.v4.MavenModelVersion;
79  import org.apache.maven.model.v4.MavenTransformer;
80  import org.eclipse.aether.scope.DependencyScope;
81  import org.eclipse.aether.scope.ScopeManager;
82  
83  /**
84   */
85  @Named
86  @Singleton
87  public class DefaultModelValidator implements ModelValidator {
88      public static final String BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION =
89              "maven.build.allowExpressionInEffectiveProjectVersion";
90  
91      private static final Pattern EXPRESSION_NAME_PATTERN = Pattern.compile("\\$\\{(.+?)}");
92      private static final Pattern EXPRESSION_PROJECT_NAME_PATTERN = Pattern.compile("\\$\\{(project.+?)}");
93  
94      private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";
95  
96      private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;
97  
98      private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;
99  
100     private static final String EMPTY = "";
101 
102     private record ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {}
103 
104     private static class ActivationWalker extends MavenTransformer {
105 
106         private final Deque<ActivationFrame> stk;
107 
108         ActivationWalker(Deque<ActivationFrame> stk, UnaryOperator<String> transformer) {
109             super(transformer);
110             this.stk = stk;
111         }
112 
113         private ActivationFrame nextFrame(String property) {
114             return new ActivationFrame(property, Optional.empty());
115         }
116 
117         private <P> ActivationFrame nextFrame(String property, Function<P, InputLocationTracker> child) {
118             @SuppressWarnings("unchecked")
119             final Optional<P> parent = (Optional<P>) stk.peek().parent;
120             return new ActivationFrame(property, parent.map(child));
121         }
122 
123         @Override
124         public Activation transformActivation(Activation target) {
125             stk.push(new ActivationFrame("activation", Optional.of(target)));
126             try {
127                 return super.transformActivation(target);
128             } finally {
129                 stk.pop();
130             }
131         }
132 
133         @Override
134         protected Activation.Builder transformActivation_File(
135                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
136             stk.push(nextFrame("file", Activation::getFile));
137             try {
138                 return super.transformActivation_File(creator, builder, target);
139             } finally {
140                 stk.pop();
141             }
142         }
143 
144         @Override
145         protected ActivationFile.Builder transformActivationFile_Exists(
146                 Supplier<? extends ActivationFile.Builder> creator,
147                 ActivationFile.Builder builder,
148                 ActivationFile target) {
149             stk.push(nextFrame("exists"));
150             try {
151                 return super.transformActivationFile_Exists(creator, builder, target);
152             } finally {
153                 stk.pop();
154             }
155         }
156 
157         @Override
158         protected ActivationFile.Builder transformActivationFile_Missing(
159                 Supplier<? extends ActivationFile.Builder> creator,
160                 ActivationFile.Builder builder,
161                 ActivationFile target) {
162             stk.push(nextFrame("missing"));
163             try {
164                 return super.transformActivationFile_Missing(creator, builder, target);
165             } finally {
166                 stk.pop();
167             }
168         }
169 
170         @Override
171         protected Activation.Builder transformActivation_Jdk(
172                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
173             stk.push(nextFrame("jdk"));
174             try {
175                 return super.transformActivation_Jdk(creator, builder, target);
176             } finally {
177                 stk.pop();
178             }
179         }
180 
181         @Override
182         protected Activation.Builder transformActivation_Os(
183                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
184             stk.push(nextFrame("os", Activation::getOs));
185             try {
186                 return super.transformActivation_Os(creator, builder, target);
187             } finally {
188                 stk.pop();
189             }
190         }
191 
192         @Override
193         protected ActivationOS.Builder transformActivationOS_Arch(
194                 Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
195             stk.push(nextFrame("arch"));
196             try {
197                 return super.transformActivationOS_Arch(creator, builder, target);
198             } finally {
199                 stk.pop();
200             }
201         }
202 
203         @Override
204         protected ActivationOS.Builder transformActivationOS_Family(
205                 Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
206             stk.push(nextFrame("family"));
207             try {
208                 return super.transformActivationOS_Family(creator, builder, target);
209             } finally {
210                 stk.pop();
211             }
212         }
213 
214         @Override
215         protected ActivationOS.Builder transformActivationOS_Name(
216                 Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
217             stk.push(nextFrame("name"));
218             try {
219                 return super.transformActivationOS_Name(creator, builder, target);
220             } finally {
221                 stk.pop();
222             }
223         }
224 
225         @Override
226         protected ActivationOS.Builder transformActivationOS_Version(
227                 Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
228             stk.push(nextFrame("version"));
229             try {
230                 return super.transformActivationOS_Version(creator, builder, target);
231             } finally {
232                 stk.pop();
233             }
234         }
235 
236         @Override
237         protected Activation.Builder transformActivation_Packaging(
238                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
239             stk.push(nextFrame("packaging"));
240             try {
241                 return super.transformActivation_Packaging(creator, builder, target);
242             } finally {
243                 stk.pop();
244             }
245         }
246 
247         @Override
248         protected Activation.Builder transformActivation_Property(
249                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
250             stk.push(nextFrame("property", Activation::getProperty));
251             try {
252                 return super.transformActivation_Property(creator, builder, target);
253             } finally {
254                 stk.pop();
255             }
256         }
257 
258         @Override
259         protected ActivationProperty.Builder transformActivationProperty_Name(
260                 Supplier<? extends ActivationProperty.Builder> creator,
261                 ActivationProperty.Builder builder,
262                 ActivationProperty target) {
263             stk.push(nextFrame("name"));
264             try {
265                 return super.transformActivationProperty_Name(creator, builder, target);
266             } finally {
267                 stk.pop();
268             }
269         }
270 
271         @Override
272         protected ActivationProperty.Builder transformActivationProperty_Value(
273                 Supplier<? extends ActivationProperty.Builder> creator,
274                 ActivationProperty.Builder builder,
275                 ActivationProperty target) {
276             stk.push(nextFrame("value"));
277             try {
278                 return super.transformActivationProperty_Value(creator, builder, target);
279             } finally {
280                 stk.pop();
281             }
282         }
283 
284         @Override
285         protected Activation.Builder transformActivation_Condition(
286                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
287             stk.push(nextFrame("condition"));
288             try {
289                 return super.transformActivation_Condition(creator, builder, target);
290             } finally {
291                 stk.pop();
292             }
293         }
294     }
295 
296     private final Set<String> validCoordinatesIds = ConcurrentHashMap.newKeySet();
297 
298     private final Set<String> validProfileIds = ConcurrentHashMap.newKeySet();
299 
300     @Inject
301     public DefaultModelValidator() {}
302 
303     @Override
304     @SuppressWarnings("checkstyle:MethodLength")
305     public void validateFileModel(Session s, Model m, int validationLevel, ModelProblemCollector problems) {
306 
307         Parent parent = m.getParent();
308         if (parent != null) {
309             validateStringNotEmpty(
310                     "parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);
311 
312             validateStringNotEmpty(
313                     "parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(), parent);
314 
315             if (equals(parent.getGroupId(), m.getGroupId()) && equals(parent.getArtifactId(), m.getArtifactId())) {
316                 addViolation(
317                         problems,
318                         Severity.FATAL,
319                         Version.BASE,
320                         "parent.artifactId",
321                         null,
322                         "must be changed"
323                                 + ", the parent element cannot have the same groupId:artifactId as the project.",
324                         parent);
325             }
326 
327             if (equals("LATEST", parent.getVersion()) || equals("RELEASE", parent.getVersion())) {
328                 addViolation(
329                         problems,
330                         Severity.WARNING,
331                         Version.BASE,
332                         "parent.version",
333                         null,
334                         "is either LATEST or RELEASE (both of them are being deprecated)",
335                         parent);
336             }
337 
338             if (parent.getRelativePath() != null
339                     && !parent.getRelativePath().isEmpty()
340                     && (parent.getGroupId() != null && !parent.getGroupId().isEmpty()
341                             || parent.getArtifactId() != null
342                                     && !parent.getArtifactId().isEmpty())
343                     && validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0
344                     && ModelBuilder.KNOWN_MODEL_VERSIONS.contains(m.getModelVersion())
345                     && !Objects.equals(m.getModelVersion(), ModelBuilder.MODEL_VERSION_4_0_0)) {
346                 addViolation(
347                         problems,
348                         Severity.WARNING,
349                         Version.BASE,
350                         "parent.relativePath",
351                         null,
352                         "only specify relativePath or groupId/artifactId in modelVersion 4.1.0",
353                         parent);
354             }
355         }
356 
357         if (validationLevel == ModelValidator.VALIDATION_LEVEL_MINIMAL) {
358             // profiles: they are essential for proper model building (may contribute profiles, dependencies...)
359             HashSet<String> minProfileIds = new HashSet<>();
360             for (Profile profile : m.getProfiles()) {
361                 if (!minProfileIds.add(profile.getId())) {
362                     addViolation(
363                             problems,
364                             Severity.WARNING,
365                             Version.BASE,
366                             "profiles.profile.id",
367                             null,
368                             "Duplicate activation for profile " + profile.getId(),
369                             profile);
370                 }
371             }
372         } else if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
373             validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.V20, m.getModelVersion(), m);
374 
375             validateModelVersion(problems, m.getModelVersion(), m, ModelBuilder.KNOWN_MODEL_VERSIONS);
376 
377             Set<String> modules = new HashSet<>();
378             for (int i = 0, n = m.getModules().size(); i < n; i++) {
379                 String module = m.getModules().get(i);
380                 if (!modules.add(module)) {
381                     addViolation(
382                             problems,
383                             Severity.ERROR,
384                             Version.V20,
385                             "modules.module[" + i + "]",
386                             null,
387                             "specifies duplicate child module " + module,
388                             m.getLocation("modules"));
389                 }
390             }
391             String modelVersion = m.getModelVersion();
392             if (Objects.equals(modelVersion, ModelBuilder.MODEL_VERSION_4_0_0)) {
393                 if (!m.getSubprojects().isEmpty()) {
394                     addViolation(
395                             problems,
396                             Severity.ERROR,
397                             Version.V40,
398                             "subprojects",
399                             null,
400                             "unexpected subprojects element",
401                             m.getLocation("subprojects"));
402                 }
403             } else {
404                 Set<String> subprojects = new HashSet<>();
405                 for (int i = 0, n = m.getSubprojects().size(); i < n; i++) {
406                     String subproject = m.getSubprojects().get(i);
407                     if (!subprojects.add(subproject)) {
408                         addViolation(
409                                 problems,
410                                 Severity.ERROR,
411                                 Version.V41,
412                                 "subprojects.subproject[" + i + "]",
413                                 null,
414                                 "specifies duplicate subproject " + subproject,
415                                 m.getLocation("subprojects"));
416                     }
417                 }
418                 if (!modules.isEmpty()) {
419                     if (subprojects.isEmpty()) {
420                         addViolation(
421                                 problems,
422                                 Severity.WARNING,
423                                 Version.V41,
424                                 "modules",
425                                 null,
426                                 "deprecated modules element, use subprojects instead",
427                                 m.getLocation("modules"));
428                     } else {
429                         addViolation(
430                                 problems,
431                                 Severity.ERROR,
432                                 Version.V41,
433                                 "modules",
434                                 null,
435                                 "cannot use both modules and subprojects element",
436                                 m.getLocation("modules"));
437                     }
438                 }
439             }
440 
441             Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
442 
443             boolean isModelVersion41OrMore = !Objects.equals(ModelBuilder.MODEL_VERSION_4_0_0, m.getModelVersion());
444             if (isModelVersion41OrMore) {
445                 validateStringNoExpression("groupId", problems, Severity.FATAL, Version.V41, m.getGroupId(), m);
446 
447                 validateStringNotEmpty("artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m);
448                 validateStringNoExpression("artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m);
449 
450                 validateVersionNoExpression("version", problems, Severity.FATAL, Version.V41, m.getVersion(), m);
451 
452                 if (parent != null) {
453                     validateStringNoExpression(
454                             "groupId", problems, Severity.FATAL, Version.V41, parent.getGroupId(), m);
455                     validateStringNoExpression(
456                             "artifactId", problems, Severity.FATAL, Version.V41, parent.getArtifactId(), m);
457                     validateVersionNoExpression(
458                             "version", problems, Severity.FATAL, Version.V41, parent.getVersion(), m);
459                 }
460             } else {
461                 validateStringNoExpression("groupId", problems, Severity.WARNING, Version.V20, m.getGroupId(), m);
462                 if (parent == null) {
463                     validateStringNotEmpty("groupId", problems, Severity.FATAL, Version.V20, m.getGroupId(), m);
464                 }
465 
466                 validateStringNoExpression("artifactId", problems, Severity.WARNING, Version.V20, m.getArtifactId(), m);
467                 validateStringNotEmpty("artifactId", problems, Severity.FATAL, Version.V20, m.getArtifactId(), m);
468 
469                 validateVersionNoExpression("version", problems, Severity.WARNING, Version.V20, m.getVersion(), m);
470                 if (parent == null) {
471                     validateStringNotEmpty("version", problems, Severity.FATAL, Version.V20, m.getVersion(), m);
472                 }
473             }
474 
475             validateStringNoExpression("packaging", problems, Severity.WARNING, Version.V20, m.getPackaging(), m);
476 
477             validate20RawDependencies(
478                     problems,
479                     m.getDependencies(),
480                     "dependencies.dependency.",
481                     EMPTY,
482                     isModelVersion41OrMore,
483                     validationLevel);
484 
485             validate20RawDependenciesSelfReferencing(problems, m, m.getDependencies(), "dependencies.dependency");
486 
487             if (m.getDependencyManagement() != null) {
488                 validate20RawDependencies(
489                         problems,
490                         m.getDependencyManagement().getDependencies(),
491                         "dependencyManagement.dependencies.dependency.",
492                         EMPTY,
493                         isModelVersion41OrMore,
494                         validationLevel);
495             }
496 
497             Build build = m.getBuild();
498             if (build != null) {
499                 validate20RawPlugins(problems, build.getPlugins(), "build.plugins.plugin.", EMPTY, validationLevel);
500 
501                 PluginManagement mgmt = build.getPluginManagement();
502                 if (mgmt != null) {
503                     validate20RawPlugins(
504                             problems,
505                             mgmt.getPlugins(),
506                             "build.pluginManagement.plugins.plugin.",
507                             EMPTY,
508                             validationLevel);
509                 }
510             }
511 
512             Set<String> profileIds = new HashSet<>();
513 
514             for (Profile profile : m.getProfiles()) {
515                 String prefix = "profiles.profile[" + profile.getId() + "].";
516 
517                 validateProfileId(prefix, "id", problems, Severity.ERROR, Version.V40, profile.getId(), null, m);
518 
519                 if (!profileIds.add(profile.getId())) {
520                     addViolation(
521                             problems,
522                             errOn30,
523                             Version.V20,
524                             "profiles.profile.id",
525                             null,
526                             "must be unique but found duplicate profile with id " + profile.getId(),
527                             profile);
528                 }
529 
530                 validate30RawProfileActivation(problems, profile.getActivation(), prefix);
531 
532                 validate20RawDependencies(
533                         problems,
534                         profile.getDependencies(),
535                         prefix,
536                         "dependencies.dependency.",
537                         isModelVersion41OrMore,
538                         validationLevel);
539 
540                 if (profile.getDependencyManagement() != null) {
541                     validate20RawDependencies(
542                             problems,
543                             profile.getDependencyManagement().getDependencies(),
544                             prefix,
545                             "dependencyManagement.dependencies.dependency.",
546                             isModelVersion41OrMore,
547                             validationLevel);
548                 }
549 
550                 BuildBase buildBase = profile.getBuild();
551                 if (buildBase != null) {
552                     validate20RawPlugins(problems, buildBase.getPlugins(), prefix, "plugins.plugin.", validationLevel);
553 
554                     PluginManagement mgmt = buildBase.getPluginManagement();
555                     if (mgmt != null) {
556                         validate20RawPlugins(
557                                 problems,
558                                 mgmt.getPlugins(),
559                                 prefix,
560                                 "pluginManagement.plugins.plugin.",
561                                 validationLevel);
562                     }
563                 }
564             }
565         }
566     }
567 
568     @Override
569     public void validateRawModel(Session s, Model m, int validationLevel, ModelProblemCollector problems) {
570         // Check that the model version is correctly set wrt the model definition, i.e., that the
571         // user does not use an attribute or element that is not available in the modelVersion used.
572         String minVersion = new MavenModelVersion().getModelVersion(m);
573         if (m.getModelVersion() != null && compareModelVersions(minVersion, m.getModelVersion()) > 0) {
574             addViolation(
575                     problems,
576                     Severity.FATAL,
577                     Version.V40,
578                     "model",
579                     null,
580                     "the model contains elements that require a model version of " + minVersion,
581                     m);
582         }
583 
584         Parent parent = m.getParent();
585 
586         if (parent != null) {
587             validateStringNotEmpty(
588                     "parent.groupId", problems, Severity.FATAL, Version.BASE, parent.getGroupId(), parent);
589 
590             validateStringNotEmpty(
591                     "parent.artifactId", problems, Severity.FATAL, Version.BASE, parent.getArtifactId(), parent);
592 
593             validateStringNotEmpty(
594                     "parent.version", problems, Severity.FATAL, Version.BASE, parent.getVersion(), parent);
595 
596             if (equals(parent.getGroupId(), m.getGroupId()) && equals(parent.getArtifactId(), m.getArtifactId())) {
597                 addViolation(
598                         problems,
599                         Severity.FATAL,
600                         Version.BASE,
601                         "parent.artifactId",
602                         null,
603                         "must be changed"
604                                 + ", the parent element cannot have the same groupId:artifactId as the project.",
605                         parent);
606             }
607 
608             if (equals("LATEST", parent.getVersion()) || equals("RELEASE", parent.getVersion())) {
609                 addViolation(
610                         problems,
611                         Severity.WARNING,
612                         Version.BASE,
613                         "parent.version",
614                         null,
615                         "is either LATEST or RELEASE (both of them are being deprecated)",
616                         parent);
617             }
618         }
619 
620         if (validationLevel > VALIDATION_LEVEL_MINIMAL) {
621             validateRawRepositories(problems, m.getRepositories(), "repositories.repository.", EMPTY, validationLevel);
622 
623             validateRawRepositories(
624                     problems,
625                     m.getPluginRepositories(),
626                     "pluginRepositories.pluginRepository.",
627                     EMPTY,
628                     validationLevel);
629 
630             for (Profile profile : m.getProfiles()) {
631                 String prefix = "profiles.profile[" + profile.getId() + "].";
632 
633                 validateRawRepositories(
634                         problems, profile.getRepositories(), prefix, "repositories.repository.", validationLevel);
635 
636                 validateRawRepositories(
637                         problems,
638                         profile.getPluginRepositories(),
639                         prefix,
640                         "pluginRepositories.pluginRepository.",
641                         validationLevel);
642             }
643 
644             DistributionManagement distMgmt = m.getDistributionManagement();
645             if (distMgmt != null) {
646                 validateRawRepository(
647                         problems, distMgmt.getRepository(), "distributionManagement.repository.", "", true);
648                 validateRawRepository(
649                         problems,
650                         distMgmt.getSnapshotRepository(),
651                         "distributionManagement.snapshotRepository.",
652                         "",
653                         true);
654             }
655         }
656     }
657 
658     private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
659         if (activation == null) {
660             return;
661         }
662 
663         final Deque<ActivationFrame> stk = new LinkedList<>();
664 
665         final Supplier<String> pathSupplier = () -> {
666             final boolean parallel = false;
667             return StreamSupport.stream(((Iterable<ActivationFrame>) stk::descendingIterator).spliterator(), parallel)
668                     .map(ActivationFrame::location)
669                     .collect(Collectors.joining("."));
670         };
671         final Supplier<InputLocation> locationSupplier = () -> {
672             if (stk.size() < 2) {
673                 return null;
674             }
675             Iterator<ActivationFrame> f = stk.iterator();
676 
677             String location = f.next().location;
678             ActivationFrame parent = f.next();
679 
680             return parent.parent.map(p -> p.getLocation(location)).orElse(null);
681         };
682         final UnaryOperator<String> transformer = s -> {
683             if (hasProjectExpression(s)) {
684                 String path = pathSupplier.get();
685                 Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(s);
686                 while (matcher.find()) {
687                     String propertyName = matcher.group(0);
688 
689                     if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) {
690                         continue;
691                     }
692                     addViolation(
693                             problems,
694                             Severity.WARNING,
695                             Version.V30,
696                             prefix + path,
697                             null,
698                             "Failed to interpolate profile activation property " + s + ": " + propertyName
699                                     + " expressions are not supported during profile activation.",
700                             locationSupplier.get());
701                 }
702             }
703             return s;
704         };
705         new ActivationWalker(stk, transformer).transformActivation(activation);
706     }
707 
708     private void validate20RawPlugins(
709             ModelProblemCollector problems, List<Plugin> plugins, String prefix, String prefix2, int validationLevel) {
710         Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
711 
712         Map<String, Plugin> index = new HashMap<>();
713 
714         for (Plugin plugin : plugins) {
715             if (plugin.getGroupId() == null
716                     || (plugin.getGroupId() != null
717                             && plugin.getGroupId().trim().isEmpty())) {
718                 addViolation(
719                         problems,
720                         Severity.FATAL,
721                         Version.V20,
722                         prefix + prefix2 + "(groupId:artifactId)",
723                         null,
724                         "groupId of a plugin must be defined. ",
725                         plugin);
726             }
727 
728             if (plugin.getArtifactId() == null
729                     || (plugin.getArtifactId() != null
730                             && plugin.getArtifactId().trim().isEmpty())) {
731                 addViolation(
732                         problems,
733                         Severity.FATAL,
734                         Version.V20,
735                         prefix + prefix2 + "(groupId:artifactId)",
736                         null,
737                         "artifactId of a plugin must be defined. ",
738                         plugin);
739             }
740 
741             // This will catch cases like <version></version> or <version/>
742             if (plugin.getVersion() != null && plugin.getVersion().trim().isEmpty()) {
743                 addViolation(
744                         problems,
745                         Severity.FATAL,
746                         Version.V20,
747                         prefix + prefix2 + "(groupId:artifactId)",
748                         null,
749                         "version of a plugin must be defined. ",
750                         plugin);
751             }
752 
753             String key = plugin.getKey();
754 
755             Plugin existing = index.get(key);
756 
757             if (existing != null) {
758                 addViolation(
759                         problems,
760                         errOn31,
761                         Version.V20,
762                         prefix + prefix2 + "(groupId:artifactId)",
763                         null,
764                         "must be unique but found duplicate declaration of plugin " + key,
765                         plugin);
766             } else {
767                 index.put(key, plugin);
768             }
769 
770             Set<String> executionIds = new HashSet<>();
771 
772             if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0 && plugin.getConfiguration() != null) {
773                 validateXmlNodeRecursively(
774                         problems,
775                         prefix + prefix2 + "[" + plugin.getKey() + "].configuration",
776                         plugin,
777                         plugin.getConfiguration());
778             }
779 
780             for (PluginExecution exec : plugin.getExecutions()) {
781                 if (!executionIds.add(exec.getId())) {
782                     addViolation(
783                             problems,
784                             Severity.ERROR,
785                             Version.V20,
786                             prefix + prefix2 + "[" + plugin.getKey() + "].executions.execution.id",
787                             null,
788                             "must be unique but found duplicate execution with id " + exec.getId(),
789                             exec);
790                 }
791                 if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_4_0 && exec.getConfiguration() != null) {
792                     validateXmlNodeRecursively(
793                             problems,
794                             prefix + prefix2 + "[" + plugin.getKey() + "].executions.execution." + exec.getId(),
795                             exec,
796                             exec.getConfiguration());
797                 }
798             }
799         }
800     }
801 
802     private void validateXmlNodeRecursively(
803             ModelProblemCollector problems, String fieldPathPrefix, InputLocationTracker tracker, XmlNode xmlNode) {
804         validateXmlNode(problems, fieldPathPrefix, tracker, xmlNode);
805         for (XmlNode child : xmlNode.children()) {
806             validateXmlNodeRecursively(problems, fieldPathPrefix + "." + xmlNode.name(), tracker, child);
807         }
808     }
809 
810     private void validateXmlNode(
811             ModelProblemCollector problems, String fieldPathPrefix, InputLocationTracker tracker, XmlNode xmlNode) {
812         String childrenCombinationModeAttribute = xmlNode.attributes()
813                 .getOrDefault(
814                         XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE, XmlService.DEFAULT_CHILDREN_COMBINATION_MODE);
815         if (!(XmlService.CHILDREN_COMBINATION_APPEND.equals(childrenCombinationModeAttribute)
816                 || XmlService.CHILDREN_COMBINATION_MERGE.equals(childrenCombinationModeAttribute))) {
817             addViolation(
818                     problems,
819                     Severity.ERROR,
820                     Version.V40,
821                     fieldPathPrefix + "." + xmlNode.name(),
822                     SourceHint.xmlNodeInputLocation(xmlNode),
823                     "Unsupported value '" + childrenCombinationModeAttribute + "' for "
824                             + XmlService.CHILDREN_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
825                             + XmlService.CHILDREN_COMBINATION_APPEND + ", and " + XmlService.CHILDREN_COMBINATION_MERGE
826                             + " (default is: " + XmlService.DEFAULT_SELF_COMBINATION_MODE + ")",
827                     tracker);
828         }
829         String selfCombinationModeAttribute = xmlNode.attributes()
830                 .getOrDefault(XmlService.SELF_COMBINATION_MODE_ATTRIBUTE, XmlService.DEFAULT_SELF_COMBINATION_MODE);
831         if (!(XmlService.SELF_COMBINATION_OVERRIDE.equals(selfCombinationModeAttribute)
832                 || XmlService.SELF_COMBINATION_MERGE.equals(selfCombinationModeAttribute)
833                 || XmlService.SELF_COMBINATION_REMOVE.equals(selfCombinationModeAttribute))) {
834             addViolation(
835                     problems,
836                     Severity.ERROR,
837                     Version.V40,
838                     fieldPathPrefix + "." + xmlNode.name(),
839                     SourceHint.xmlNodeInputLocation(xmlNode),
840                     "Unsupported value '" + selfCombinationModeAttribute + "' for "
841                             + XmlService.SELF_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
842                             + XmlService.SELF_COMBINATION_OVERRIDE + ", " + XmlService.SELF_COMBINATION_MERGE + ", and "
843                             + XmlService.SELF_COMBINATION_REMOVE
844                             + " (default is: " + XmlService.DEFAULT_SELF_COMBINATION_MODE + ")",
845                     tracker);
846         }
847     }
848 
849     @Override
850     @SuppressWarnings("checkstyle:MethodLength")
851     public void validateEffectiveModel(Session s, Model m, int validationLevel, ModelProblemCollector problems) {
852         validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, m.getModelVersion(), m);
853 
854         validateCoordinatesId("groupId", problems, m.getGroupId(), m);
855 
856         validateCoordinatesId("artifactId", problems, m.getArtifactId(), m);
857 
858         validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, m.getPackaging(), m);
859 
860         if (!m.getModules().isEmpty() || !m.getSubprojects().isEmpty()) {
861             if (!"pom".equals(m.getPackaging())) {
862                 addViolation(
863                         problems,
864                         Severity.ERROR,
865                         Version.BASE,
866                         "packaging",
867                         null,
868                         "with value '" + m.getPackaging() + "' is invalid. Aggregator projects "
869                                 + "require 'pom' as packaging.",
870                         m);
871             }
872 
873             for (int i = 0, n = m.getModules().size(); i < n; i++) {
874                 String module = m.getModules().get(i);
875 
876                 boolean isBlankModule = true;
877                 if (module != null) {
878                     for (int j = 0; j < module.length(); j++) {
879                         if (!Character.isWhitespace(module.charAt(j))) {
880                             isBlankModule = false;
881                         }
882                     }
883                 }
884 
885                 if (isBlankModule) {
886                     addViolation(
887                             problems,
888                             Severity.ERROR,
889                             Version.BASE,
890                             "modules.module[" + i + "]",
891                             null,
892                             "has been specified without a path to the project directory.",
893                             m.getLocation("modules"));
894                 }
895             }
896 
897             for (int index = 0, size = m.getSubprojects().size(); index < size; index++) {
898                 String subproject = m.getSubprojects().get(index);
899 
900                 boolean isBlankSubproject = true;
901                 if (subproject != null) {
902                     for (int charIndex = 0; charIndex < subproject.length(); charIndex++) {
903                         if (!Character.isWhitespace(subproject.charAt(charIndex))) {
904                             isBlankSubproject = false;
905                         }
906                     }
907                 }
908 
909                 if (isBlankSubproject) {
910                     addViolation(
911                             problems,
912                             Severity.ERROR,
913                             Version.BASE,
914                             "subprojects.subproject[" + index + "]",
915                             null,
916                             "has been specified without a path to the project directory.",
917                             m.getLocation("subprojects"));
918                 }
919             }
920         }
921 
922         validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, m.getVersion(), m);
923 
924         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
925 
926         validateEffectiveDependencies(s, problems, m, m.getDependencies(), false, validationLevel);
927 
928         DependencyManagement mgmt = m.getDependencyManagement();
929         if (mgmt != null) {
930             validateEffectiveDependencies(s, problems, m, mgmt.getDependencies(), true, validationLevel);
931         }
932 
933         if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
934             Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
935 
936             validateBannedCharacters(
937                     EMPTY, "version", problems, errOn31, Version.V20, m.getVersion(), null, m, ILLEGAL_VERSION_CHARS);
938             validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, m.getVersion(), null, m);
939             if (hasExpression(m.getVersion())) {
940                 Severity versionExpressionSeverity = Severity.ERROR;
941                 if (m.getProperties() != null
942                         && Boolean.parseBoolean(
943                                 m.getProperties().get(BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION))) {
944                     versionExpressionSeverity = Severity.WARNING;
945                 }
946                 addViolation(
947                         problems,
948                         versionExpressionSeverity,
949                         Version.V20,
950                         "version",
951                         null,
952                         "must be a constant version but is '" + m.getVersion() + "'.",
953                         m);
954             }
955 
956             Build build = m.getBuild();
957             if (build != null) {
958                 for (Plugin p : build.getPlugins()) {
959                     validateStringNotEmpty(
960                             "build.plugins.plugin.artifactId",
961                             problems,
962                             Severity.ERROR,
963                             Version.V20,
964                             p.getArtifactId(),
965                             p);
966 
967                     validateStringNotEmpty(
968                             "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(), p);
969 
970                     validate20PluginVersion(
971                             "build.plugins.plugin.version",
972                             problems,
973                             p.getVersion(),
974                             SourceHint.pluginKey(p),
975                             p,
976                             validationLevel);
977 
978                     validateBoolean(
979                             "build.plugins.plugin.inherited",
980                             EMPTY,
981                             problems,
982                             errOn30,
983                             Version.V20,
984                             p.getInherited(),
985                             SourceHint.pluginKey(p),
986                             p);
987 
988                     validateBoolean(
989                             "build.plugins.plugin.extensions",
990                             EMPTY,
991                             problems,
992                             errOn30,
993                             Version.V20,
994                             p.getExtensions(),
995                             SourceHint.pluginKey(p),
996                             p);
997 
998                     validate20EffectivePluginDependencies(problems, p, validationLevel);
999                 }
1000 
1001                 validate20RawResources(problems, build.getResources(), "build.resources.resource.", validationLevel);
1002 
1003                 validate20RawResources(
1004                         problems, build.getTestResources(), "build.testResources.testResource.", validationLevel);
1005             }
1006 
1007             Reporting reporting = m.getReporting();
1008             if (reporting != null) {
1009                 for (ReportPlugin p : reporting.getPlugins()) {
1010                     validateStringNotEmpty(
1011                             "reporting.plugins.plugin.artifactId",
1012                             problems,
1013                             Severity.ERROR,
1014                             Version.V20,
1015                             p.getArtifactId(),
1016                             p);
1017 
1018                     validateStringNotEmpty(
1019                             "reporting.plugins.plugin.groupId",
1020                             problems,
1021                             Severity.ERROR,
1022                             Version.V20,
1023                             p.getGroupId(),
1024                             p);
1025                 }
1026             }
1027 
1028             for (Repository repository : m.getRepositories()) {
1029                 validate20EffectiveRepository(problems, repository, "repositories.repository.", validationLevel);
1030             }
1031 
1032             for (Repository repository : m.getPluginRepositories()) {
1033                 validate20EffectiveRepository(
1034                         problems, repository, "pluginRepositories.pluginRepository.", validationLevel);
1035             }
1036 
1037             DistributionManagement distMgmt = m.getDistributionManagement();
1038             if (distMgmt != null) {
1039                 if (distMgmt.getStatus() != null) {
1040                     addViolation(
1041                             problems,
1042                             Severity.ERROR,
1043                             Version.V20,
1044                             "distributionManagement.status",
1045                             null,
1046                             "must not be specified.",
1047                             distMgmt);
1048                 }
1049 
1050                 validate20EffectiveRepository(
1051                         problems, distMgmt.getRepository(), "distributionManagement.repository.", validationLevel);
1052                 validate20EffectiveRepository(
1053                         problems,
1054                         distMgmt.getSnapshotRepository(),
1055                         "distributionManagement.snapshotRepository.",
1056                         validationLevel);
1057             }
1058         }
1059     }
1060 
1061     private void validate20RawDependencies(
1062             ModelProblemCollector problems,
1063             List<Dependency> dependencies,
1064             String prefix,
1065             String prefix2,
1066             boolean is41OrBeyond,
1067             int validationLevel) {
1068         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1069         Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
1070 
1071         Map<String, Dependency> index = new HashMap<>();
1072 
1073         for (Dependency dependency : dependencies) {
1074             String key = dependency.getManagementKey();
1075 
1076             if ("import".equals(dependency.getScope())) {
1077                 if (!"pom".equals(dependency.getType())) {
1078                     addViolation(
1079                             problems,
1080                             Severity.WARNING,
1081                             Version.V20,
1082                             prefix + prefix2 + "type",
1083                             SourceHint.dependencyManagementKey(dependency),
1084                             "must be 'pom' to import the managed dependencies.",
1085                             dependency);
1086                 } else if (!is41OrBeyond
1087                         && dependency.getClassifier() != null
1088                         && !dependency.getClassifier().isEmpty()) {
1089                     addViolation(
1090                             problems,
1091                             errOn30,
1092                             Version.V20,
1093                             prefix + prefix2 + "classifier",
1094                             SourceHint.dependencyManagementKey(dependency),
1095                             "must be empty, imported POM cannot have a classifier.",
1096                             dependency);
1097                 }
1098             } else if ("system".equals(dependency.getScope())) {
1099 
1100                 if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_3_1) {
1101                     addViolation(
1102                             problems,
1103                             Severity.WARNING,
1104                             Version.V31,
1105                             prefix + prefix2 + "scope",
1106                             SourceHint.dependencyManagementKey(dependency),
1107                             "declares usage of deprecated 'system' scope ",
1108                             dependency);
1109                 }
1110 
1111                 String sysPath = dependency.getSystemPath();
1112                 if (sysPath != null && !sysPath.isEmpty()) {
1113                     if (!hasExpression(sysPath)) {
1114                         addViolation(
1115                                 problems,
1116                                 Severity.WARNING,
1117                                 Version.V20,
1118                                 prefix + prefix2 + "systemPath",
1119                                 SourceHint.dependencyManagementKey(dependency),
1120                                 "should use a variable instead of a hard-coded path " + sysPath,
1121                                 dependency);
1122                     } else if (sysPath.contains("${basedir}") || sysPath.contains("${project.basedir}")) {
1123                         addViolation(
1124                                 problems,
1125                                 Severity.WARNING,
1126                                 Version.V20,
1127                                 prefix + prefix2 + "systemPath",
1128                                 SourceHint.dependencyManagementKey(dependency),
1129                                 "should not point at files within the project directory, " + sysPath
1130                                         + " will be unresolvable by dependent projects",
1131                                 dependency);
1132                     }
1133                 }
1134             }
1135 
1136             if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) {
1137                 addViolation(
1138                         problems,
1139                         Severity.WARNING,
1140                         Version.BASE,
1141                         prefix + prefix2 + "version",
1142                         SourceHint.dependencyManagementKey(dependency),
1143                         "is either LATEST or RELEASE (both of them are being deprecated)",
1144                         dependency);
1145             }
1146 
1147             Dependency existing = index.get(key);
1148 
1149             if (existing != null) {
1150                 String msg;
1151                 if (equals(existing.getVersion(), dependency.getVersion())) {
1152                     msg = "duplicate declaration of version " + Objects.toString(dependency.getVersion(), "(?)");
1153                 } else {
1154                     msg = "version " + Objects.toString(existing.getVersion(), "(?)") + " vs "
1155                             + Objects.toString(dependency.getVersion(), "(?)");
1156                 }
1157 
1158                 addViolation(
1159                         problems,
1160                         errOn31,
1161                         Version.V20,
1162                         prefix + prefix2 + "(groupId:artifactId:type:classifier)",
1163                         null,
1164                         "must be unique: " + key + " -> " + msg,
1165                         dependency);
1166             } else {
1167                 index.put(key, dependency);
1168             }
1169         }
1170     }
1171 
1172     private void validate20RawDependenciesSelfReferencing(
1173             ModelProblemCollector problems, Model m, List<Dependency> dependencies, String prefix) {
1174         // We only check for groupId/artifactId/version/classifier cause if there is another
1175         // module with the same groupId/artifactId/version/classifier this will fail the build
1176         // earlier like "Project '...' is duplicated in the reactor.
1177         // So it is sufficient to check only groupId/artifactId/version/classifier and not the
1178         // packaging type.
1179         for (Dependency dependency : dependencies) {
1180             String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
1181                     + (dependency.getClassifier() != null ? ":" + dependency.getClassifier() : EMPTY);
1182             String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
1183             if (key.equals(mKey)) {
1184                 // This means a module which is build has a dependency which has the same
1185                 // groupId, artifactId, version and classifier coordinates. This is in consequence
1186                 // a self reference or in other words a circular reference which can not being resolved.
1187                 addViolation(
1188                         problems,
1189                         Severity.FATAL,
1190                         Version.V31,
1191                         prefix + "[" + key + "]",
1192                         SourceHint.gav(key),
1193                         "is referencing itself.",
1194                         dependency);
1195             }
1196         }
1197     }
1198 
1199     private void validateEffectiveDependencies(
1200             Session s,
1201             ModelProblemCollector problems,
1202             Model m,
1203             List<Dependency> dependencies,
1204             boolean management,
1205             int validationLevel) {
1206         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1207 
1208         String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";
1209 
1210         for (Dependency d : dependencies) {
1211             validateEffectiveDependency(problems, d, management, prefix, validationLevel);
1212 
1213             if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
1214                 validateBoolean(
1215                         prefix,
1216                         "optional",
1217                         problems,
1218                         errOn30,
1219                         Version.V20,
1220                         d.getOptional(),
1221                         SourceHint.dependencyManagementKey(d),
1222                         d);
1223 
1224                 if (!management) {
1225                     validateVersion(
1226                             prefix,
1227                             "version",
1228                             problems,
1229                             errOn30,
1230                             Version.V20,
1231                             d.getVersion(),
1232                             SourceHint.dependencyManagementKey(d),
1233                             d);
1234 
1235                     /*
1236                      * TODO Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In
1237                      * order to don't break backward-compat with those, only warn but don't error out.
1238                      */
1239                     ScopeManager scopeManager =
1240                             InternalSession.from(s).getSession().getScopeManager();
1241                     validateEnum(
1242                             prefix,
1243                             "scope",
1244                             problems,
1245                             Severity.WARNING,
1246                             Version.V20,
1247                             d.getScope(),
1248                             SourceHint.dependencyManagementKey(d),
1249                             d,
1250                             scopeManager.getDependencyScopeUniverse().stream()
1251                                     .map(DependencyScope::getId)
1252                                     .distinct()
1253                                     .toArray(String[]::new));
1254 
1255                     validateEffectiveModelAgainstDependency(prefix, problems, m, d);
1256                 } else {
1257                     ScopeManager scopeManager =
1258                             InternalSession.from(s).getSession().getScopeManager();
1259                     Set<String> scopes = scopeManager.getDependencyScopeUniverse().stream()
1260                             .map(DependencyScope::getId)
1261                             .collect(Collectors.toCollection(HashSet::new));
1262                     scopes.add("import");
1263                     validateEnum(
1264                             prefix,
1265                             "scope",
1266                             problems,
1267                             Severity.WARNING,
1268                             Version.V20,
1269                             d.getScope(),
1270                             SourceHint.dependencyManagementKey(d),
1271                             d,
1272                             scopes.toArray(new String[0]));
1273                 }
1274             }
1275         }
1276     }
1277 
1278     private void validateEffectiveModelAgainstDependency(
1279             String prefix, ModelProblemCollector problems, Model m, Dependency d) {
1280         String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion()
1281                 + (d.getClassifier() != null ? ":" + d.getClassifier() : EMPTY);
1282         String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
1283         if (key.equals(mKey)) {
1284             // This means a module which is build has a dependency which has the same
1285             // groupId, artifactId, version and classifier coordinates. This is in consequence
1286             // a self reference or in other words a circular reference which can not being resolved.
1287             addViolation(
1288                     problems,
1289                     Severity.FATAL,
1290                     Version.V31,
1291                     prefix + "[" + key + "]",
1292                     SourceHint.gav(key),
1293                     "is referencing itself.",
1294                     d);
1295         }
1296     }
1297 
1298     private void validate20EffectivePluginDependencies(
1299             ModelProblemCollector problems, Plugin plugin, int validationLevel) {
1300         List<Dependency> dependencies = plugin.getDependencies();
1301 
1302         if (!dependencies.isEmpty()) {
1303             String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
1304 
1305             Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1306 
1307             for (Dependency d : dependencies) {
1308                 validateEffectiveDependency(problems, d, false, prefix, validationLevel);
1309 
1310                 validateVersion(
1311                         prefix,
1312                         "version",
1313                         problems,
1314                         errOn30,
1315                         Version.BASE,
1316                         d.getVersion(),
1317                         SourceHint.dependencyManagementKey(d),
1318                         d);
1319 
1320                 validateEnum(
1321                         prefix,
1322                         "scope",
1323                         problems,
1324                         errOn30,
1325                         Version.BASE,
1326                         d.getScope(),
1327                         SourceHint.dependencyManagementKey(d),
1328                         d,
1329                         "compile",
1330                         "runtime",
1331                         "system");
1332             }
1333         }
1334     }
1335 
1336     private void validateEffectiveDependency(
1337             ModelProblemCollector problems, Dependency d, boolean management, String prefix, int validationLevel) {
1338         validateCoordinatesId(
1339                 prefix,
1340                 "artifactId",
1341                 problems,
1342                 Severity.ERROR,
1343                 Version.BASE,
1344                 d.getArtifactId(),
1345                 SourceHint.dependencyManagementKey(d),
1346                 d);
1347 
1348         validateCoordinatesId(
1349                 prefix,
1350                 "groupId",
1351                 problems,
1352                 Severity.ERROR,
1353                 Version.BASE,
1354                 d.getGroupId(),
1355                 SourceHint.dependencyManagementKey(d),
1356                 d);
1357 
1358         if (!management) {
1359             validateStringNotEmpty(
1360                     prefix,
1361                     "type",
1362                     problems,
1363                     Severity.ERROR,
1364                     Version.BASE,
1365                     d.getType(),
1366                     SourceHint.dependencyManagementKey(d),
1367                     d);
1368 
1369             validateDependencyVersion(problems, d, prefix);
1370         }
1371 
1372         if ("system".equals(d.getScope())) {
1373             String systemPath = d.getSystemPath();
1374 
1375             if (systemPath == null || systemPath.isEmpty()) {
1376                 addViolation(
1377                         problems,
1378                         Severity.ERROR,
1379                         Version.BASE,
1380                         prefix + "systemPath",
1381                         SourceHint.dependencyManagementKey(d),
1382                         "is missing.",
1383                         d);
1384             } else {
1385                 File sysFile = new File(systemPath);
1386                 if (!sysFile.isAbsolute()) {
1387                     addViolation(
1388                             problems,
1389                             Severity.ERROR,
1390                             Version.BASE,
1391                             prefix + "systemPath",
1392                             SourceHint.dependencyManagementKey(d),
1393                             "must specify an absolute path but is " + systemPath,
1394                             d);
1395                 } else if (!sysFile.isFile()) {
1396                     String msg = "refers to a non-existing file " + sysFile.getAbsolutePath() + ".";
1397                     addViolation(
1398                             problems,
1399                             Severity.WARNING,
1400                             Version.BASE,
1401                             prefix + "systemPath",
1402                             SourceHint.dependencyManagementKey(d),
1403                             msg,
1404                             d);
1405                 }
1406             }
1407         } else if (d.getSystemPath() != null && !d.getSystemPath().isEmpty()) {
1408             addViolation(
1409                     problems,
1410                     Severity.ERROR,
1411                     Version.BASE,
1412                     prefix + "systemPath",
1413                     SourceHint.dependencyManagementKey(d),
1414                     "must be omitted. This field may only be specified for a dependency with system scope.",
1415                     d);
1416         }
1417 
1418         if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
1419             for (Exclusion exclusion : d.getExclusions()) {
1420                 if (validationLevel < ModelValidator.VALIDATION_LEVEL_MAVEN_3_0) {
1421                     validateCoordinatesId(
1422                             prefix,
1423                             "exclusions.exclusion.groupId",
1424                             problems,
1425                             Severity.WARNING,
1426                             Version.V20,
1427                             exclusion.getGroupId(),
1428                             SourceHint.dependencyManagementKey(d),
1429                             exclusion);
1430 
1431                     validateCoordinatesId(
1432                             prefix,
1433                             "exclusions.exclusion.artifactId",
1434                             problems,
1435                             Severity.WARNING,
1436                             Version.V20,
1437                             exclusion.getArtifactId(),
1438                             SourceHint.dependencyManagementKey(d),
1439                             exclusion);
1440                 } else {
1441                     validateCoordinatesIdWithWildcards(
1442                             prefix,
1443                             "exclusions.exclusion.groupId",
1444                             problems,
1445                             Severity.WARNING,
1446                             Version.V30,
1447                             exclusion.getGroupId(),
1448                             SourceHint.dependencyManagementKey(d),
1449                             exclusion);
1450 
1451                     validateCoordinatesIdWithWildcards(
1452                             prefix,
1453                             "exclusions.exclusion.artifactId",
1454                             problems,
1455                             Severity.WARNING,
1456                             Version.V30,
1457                             exclusion.getArtifactId(),
1458                             SourceHint.dependencyManagementKey(d),
1459                             exclusion);
1460                 }
1461             }
1462         }
1463     }
1464 
1465     /**
1466      * @since 3.2.4
1467      */
1468     protected void validateDependencyVersion(ModelProblemCollector problems, Dependency d, String prefix) {
1469         validateStringNotEmpty(
1470                 prefix,
1471                 "version",
1472                 problems,
1473                 Severity.ERROR,
1474                 Version.BASE,
1475                 d.getVersion(),
1476                 SourceHint.dependencyManagementKey(d),
1477                 d);
1478     }
1479 
1480     private void validateRawRepositories(
1481             ModelProblemCollector problems,
1482             List<Repository> repositories,
1483             String prefix,
1484             String prefix2,
1485             int validationLevel) {
1486         Map<String, Repository> index = new HashMap<>();
1487 
1488         for (Repository repository : repositories) {
1489             validateRawRepository(problems, repository, prefix, prefix2, false);
1490 
1491             String key = repository.getId();
1492 
1493             Repository existing = index.get(key);
1494 
1495             if (existing != null) {
1496                 Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1497 
1498                 addViolation(
1499                         problems,
1500                         errOn30,
1501                         Version.V20,
1502                         prefix + prefix2 + "id",
1503                         null,
1504                         "must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs "
1505                                 + repository.getUrl(),
1506                         repository);
1507             } else {
1508                 index.put(key, repository);
1509             }
1510         }
1511     }
1512 
1513     private void validateRawRepository(
1514             ModelProblemCollector problems,
1515             Repository repository,
1516             String prefix,
1517             String prefix2,
1518             boolean allowEmptyUrl) {
1519         if (repository == null) {
1520             return;
1521         }
1522         if (validateStringNotEmpty(
1523                 prefix, prefix2, "id", problems, Severity.ERROR, Version.V20, repository.getId(), null, repository)) {
1524             // Check for uninterpolated expressions in ID - these should have been interpolated by now
1525             Matcher matcher = EXPRESSION_NAME_PATTERN.matcher(repository.getId());
1526             if (matcher.find()) {
1527                 addViolation(
1528                         problems,
1529                         Severity.ERROR,
1530                         Version.V40,
1531                         prefix + prefix2 + "[" + repository.getId() + "].id",
1532                         null,
1533                         "contains an uninterpolated expression.",
1534                         repository);
1535             }
1536         }
1537 
1538         if (!allowEmptyUrl
1539                 && validateStringNotEmpty(
1540                         prefix,
1541                         prefix2,
1542                         "[" + repository.getId() + "].url",
1543                         problems,
1544                         Severity.ERROR,
1545                         Version.V20,
1546                         repository.getUrl(),
1547                         null,
1548                         repository)) {
1549             // Check for uninterpolated expressions in URL - these should have been interpolated by now
1550             Matcher matcher = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
1551             if (matcher.find()) {
1552                 addViolation(
1553                         problems,
1554                         Severity.ERROR,
1555                         Version.V40,
1556                         prefix + prefix2 + "[" + repository.getId() + "].url",
1557                         null,
1558                         "contains an uninterpolated expression.",
1559                         repository);
1560             }
1561         }
1562     }
1563 
1564     private void validate20EffectiveRepository(
1565             ModelProblemCollector problems, Repository repository, String prefix, int validationLevel) {
1566         if (repository != null) {
1567             Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
1568 
1569             validateBannedCharacters(
1570                     prefix,
1571                     "id",
1572                     problems,
1573                     errOn31,
1574                     Version.V20,
1575                     repository.getId(),
1576                     null,
1577                     repository,
1578                     ILLEGAL_REPO_ID_CHARS);
1579 
1580             if ("local".equals(repository.getId())) {
1581                 addViolation(
1582                         problems,
1583                         errOn31,
1584                         Version.V20,
1585                         prefix + "id",
1586                         null,
1587                         "must not be 'local'" + ", this identifier is reserved for the local repository"
1588                                 + ", using it for other repositories will corrupt your repository metadata.",
1589                         repository);
1590             }
1591 
1592             if ("legacy".equals(repository.getLayout())) {
1593                 addViolation(
1594                         problems,
1595                         Severity.WARNING,
1596                         Version.V20,
1597                         prefix + "layout",
1598                         SourceHint.repoId(repository),
1599                         "uses the unsupported value 'legacy', artifact resolution might fail.",
1600                         repository);
1601             }
1602         }
1603     }
1604 
1605     private void validate20RawResources(
1606             ModelProblemCollector problems, List<Resource> resources, String prefix, int validationLevel) {
1607         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1608 
1609         for (Resource resource : resources) {
1610             validateStringNotEmpty(
1611                     prefix,
1612                     "directory",
1613                     problems,
1614                     Severity.ERROR,
1615                     Version.V20,
1616                     resource.getDirectory(),
1617                     null,
1618                     resource);
1619 
1620             validateBoolean(
1621                     prefix,
1622                     "filtering",
1623                     problems,
1624                     errOn30,
1625                     Version.V20,
1626                     resource.getFiltering(),
1627                     SourceHint.resourceDirectory(resource),
1628                     resource);
1629         }
1630     }
1631 
1632     // ----------------------------------------------------------------------
1633     // Field validation
1634     // ----------------------------------------------------------------------
1635 
1636     private boolean validateCoordinatesId(
1637             String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) {
1638         return validateCoordinatesId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
1639     }
1640 
1641     @SuppressWarnings("checkstyle:parameternumber")
1642     private boolean validateCoordinatesId(
1643             String prefix,
1644             String fieldName,
1645             ModelProblemCollector problems,
1646             Severity severity,
1647             Version version,
1648             String id,
1649             @Nullable SourceHint sourceHint,
1650             InputLocationTracker tracker) {
1651         if (id != null && validCoordinatesIds.contains(id)) {
1652             return true;
1653         }
1654         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1655             return false;
1656         } else {
1657             if (!isValidCoordinatesId(id)) {
1658                 addViolation(
1659                         problems,
1660                         severity,
1661                         version,
1662                         prefix + fieldName,
1663                         sourceHint,
1664                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1665                         tracker);
1666                 return false;
1667             }
1668             validCoordinatesIds.add(id);
1669             return true;
1670         }
1671     }
1672 
1673     private boolean isValidCoordinatesId(String id) {
1674         for (int i = 0; i < id.length(); i++) {
1675             char c = id.charAt(i);
1676             if (!isValidCoordinatesIdCharacter(c)) {
1677                 return false;
1678             }
1679         }
1680         return true;
1681     }
1682 
1683     private boolean isValidCoordinatesIdCharacter(char c) {
1684         return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.';
1685     }
1686 
1687     @SuppressWarnings("checkstyle:parameternumber")
1688     private boolean validateProfileId(
1689             String prefix,
1690             String fieldName,
1691             ModelProblemCollector problems,
1692             Severity severity,
1693             Version version,
1694             String id,
1695             @Nullable SourceHint sourceHint,
1696             InputLocationTracker tracker) {
1697         if (validProfileIds.contains(id)) {
1698             return true;
1699         }
1700         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1701             return false;
1702         } else {
1703             if (!isValidProfileId(id)) {
1704                 addViolation(
1705                         problems,
1706                         severity,
1707                         version,
1708                         prefix + fieldName,
1709                         sourceHint,
1710                         "with value '" + id + "' does not match a valid profile id pattern.",
1711                         tracker);
1712                 return false;
1713             }
1714             validProfileIds.add(id);
1715             return true;
1716         }
1717     }
1718 
1719     private boolean isValidProfileId(String id) {
1720         return switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx"
1721             // +: activate
1722             // -, !: deactivate
1723             // ?: optional
1724             case '+', '-', '!', '?' -> false;
1725             default -> true;
1726         };
1727     }
1728 
1729     @SuppressWarnings("checkstyle:parameternumber")
1730     private boolean validateCoordinatesIdWithWildcards(
1731             String prefix,
1732             String fieldName,
1733             ModelProblemCollector problems,
1734             Severity severity,
1735             Version version,
1736             String id,
1737             @Nullable SourceHint sourceHint,
1738             InputLocationTracker tracker) {
1739         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1740             return false;
1741         } else {
1742             if (!isValidCoordinatesIdWithWildCards(id)) {
1743                 addViolation(
1744                         problems,
1745                         severity,
1746                         version,
1747                         prefix + fieldName,
1748                         sourceHint,
1749                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1750                         tracker);
1751                 return false;
1752             }
1753             return true;
1754         }
1755     }
1756 
1757     private boolean isValidCoordinatesIdWithWildCards(String id) {
1758         for (int i = 0; i < id.length(); i++) {
1759             char c = id.charAt(i);
1760             if (!isValidCoordinatesIdWithWildCardCharacter(c)) {
1761                 return false;
1762             }
1763         }
1764         return true;
1765     }
1766 
1767     private boolean isValidCoordinatesIdWithWildCardCharacter(char c) {
1768         return isValidCoordinatesIdCharacter(c) || c == '?' || c == '*';
1769     }
1770 
1771     private boolean validateStringNoExpression(
1772             String fieldName,
1773             ModelProblemCollector problems,
1774             Severity severity,
1775             Version version,
1776             String string,
1777             InputLocationTracker tracker) {
1778         if (!hasExpression(string)) {
1779             return true;
1780         }
1781 
1782         addViolation(
1783                 problems,
1784                 severity,
1785                 version,
1786                 fieldName,
1787                 null,
1788                 "contains an expression but should be a constant.",
1789                 tracker);
1790 
1791         return false;
1792     }
1793 
1794     private boolean validateVersionNoExpression(
1795             String fieldName,
1796             ModelProblemCollector problems,
1797             Severity severity,
1798             Version version,
1799             String string,
1800             InputLocationTracker tracker) {
1801         if (!hasExpression(string)) {
1802             return true;
1803         }
1804 
1805         Matcher m = EXPRESSION_NAME_PATTERN.matcher(string.trim());
1806         if (m.find()) {
1807             addViolation(
1808                     problems,
1809                     severity,
1810                     version,
1811                     fieldName,
1812                     null,
1813                     "contains an expression but should be a constant.",
1814                     tracker);
1815         }
1816 
1817         return true;
1818     }
1819 
1820     private boolean hasExpression(String value) {
1821         return value != null && value.contains("${");
1822     }
1823 
1824     private boolean hasProjectExpression(String value) {
1825         return value != null && value.contains("${project.");
1826     }
1827 
1828     private boolean validateStringNotEmpty(
1829             String fieldName,
1830             ModelProblemCollector problems,
1831             Severity severity,
1832             Version version,
1833             String string,
1834             InputLocationTracker tracker) {
1835         return validateStringNotEmpty(EMPTY, fieldName, problems, severity, version, string, null, tracker);
1836     }
1837 
1838     /**
1839      * Asserts:
1840      * <p/>
1841      * <ul>
1842      * <li><code>string != null</code>
1843      * <li><code>string.length > 0</code>
1844      * </ul>
1845      */
1846     @SuppressWarnings("checkstyle:parameternumber")
1847     private boolean validateStringNotEmpty(
1848             String prefix,
1849             String prefix2,
1850             String fieldName,
1851             ModelProblemCollector problems,
1852             Severity severity,
1853             Version version,
1854             String string,
1855             @Nullable SourceHint sourceHint,
1856             InputLocationTracker tracker) {
1857         if (!validateNotNull(prefix, prefix2, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1858             return false;
1859         }
1860 
1861         if (!string.isEmpty()) {
1862             return true;
1863         }
1864 
1865         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1866 
1867         return false;
1868     }
1869 
1870     /**
1871      * Asserts:
1872      * <p/>
1873      * <ul>
1874      * <li><code>string != null</code>
1875      * <li><code>string.length > 0</code>
1876      * </ul>
1877      */
1878     @SuppressWarnings("checkstyle:parameternumber")
1879     private boolean validateStringNotEmpty(
1880             String prefix,
1881             String fieldName,
1882             ModelProblemCollector problems,
1883             Severity severity,
1884             Version version,
1885             String string,
1886             @Nullable SourceHint sourceHint,
1887             InputLocationTracker tracker) {
1888         if (!validateNotNull(prefix, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1889             return false;
1890         }
1891 
1892         if (!string.isEmpty()) {
1893             return true;
1894         }
1895 
1896         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1897 
1898         return false;
1899     }
1900 
1901     /**
1902      * Asserts:
1903      * <p/>
1904      * <ul>
1905      * <li><code>string != null</code>
1906      * </ul>
1907      */
1908     @SuppressWarnings("checkstyle:parameternumber")
1909     private boolean validateNotNull(
1910             String prefix,
1911             String fieldName,
1912             ModelProblemCollector problems,
1913             Severity severity,
1914             Version version,
1915             Object object,
1916             @Nullable SourceHint sourceHint,
1917             InputLocationTracker tracker) {
1918         if (object != null) {
1919             return true;
1920         }
1921 
1922         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1923 
1924         return false;
1925     }
1926 
1927     /**
1928      * Asserts:
1929      * <p/>
1930      * <ul>
1931      * <li><code>string != null</code>
1932      * </ul>
1933      */
1934     @SuppressWarnings("checkstyle:parameternumber")
1935     private boolean validateNotNull(
1936             String prefix,
1937             String prefix2,
1938             String fieldName,
1939             ModelProblemCollector problems,
1940             Severity severity,
1941             Version version,
1942             Object object,
1943             @Nullable SourceHint sourceHint,
1944             InputLocationTracker tracker) {
1945         if (object != null) {
1946             return true;
1947         }
1948 
1949         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1950 
1951         return false;
1952     }
1953 
1954     @SuppressWarnings("checkstyle:parameternumber")
1955     private boolean validateBoolean(
1956             String prefix,
1957             String fieldName,
1958             ModelProblemCollector problems,
1959             Severity severity,
1960             Version version,
1961             String string,
1962             @Nullable SourceHint sourceHint,
1963             InputLocationTracker tracker) {
1964         if (string == null || string.isEmpty()) {
1965             return true;
1966         }
1967 
1968         if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
1969             return true;
1970         }
1971 
1972         addViolation(
1973                 problems,
1974                 severity,
1975                 version,
1976                 prefix + fieldName,
1977                 sourceHint,
1978                 "must be 'true' or 'false' but is '" + string + "'.",
1979                 tracker);
1980 
1981         return false;
1982     }
1983 
1984     @SuppressWarnings("checkstyle:parameternumber")
1985     private boolean validateEnum(
1986             String prefix,
1987             String fieldName,
1988             ModelProblemCollector problems,
1989             Severity severity,
1990             Version version,
1991             String string,
1992             @Nullable SourceHint sourceHint,
1993             InputLocationTracker tracker,
1994             String... validValues) {
1995         if (string == null || string.isEmpty()) {
1996             return true;
1997         }
1998 
1999         List<String> values = Arrays.asList(validValues);
2000 
2001         if (values.contains(string)) {
2002             return true;
2003         }
2004 
2005         addViolation(
2006                 problems,
2007                 severity,
2008                 version,
2009                 prefix + fieldName,
2010                 sourceHint,
2011                 "must be one of " + values + " but is '" + string + "'.",
2012                 tracker);
2013 
2014         return false;
2015     }
2016 
2017     @SuppressWarnings("checkstyle:parameternumber")
2018     private boolean validateModelVersion(
2019             ModelProblemCollector problems, String string, InputLocationTracker tracker, List<String> validVersions) {
2020         if (string == null || string.isEmpty()) {
2021             return true;
2022         }
2023 
2024         if (validVersions.contains(string)) {
2025             return true;
2026         }
2027 
2028         boolean newerThanAll = true;
2029         boolean olderThanAll = true;
2030         for (String validValue : validVersions) {
2031             final int comparison = compareModelVersions(validValue, string);
2032             newerThanAll = newerThanAll && comparison < 0;
2033             olderThanAll = olderThanAll && comparison > 0;
2034         }
2035 
2036         if (newerThanAll) {
2037             addViolation(
2038                     problems,
2039                     Severity.FATAL,
2040                     Version.V20,
2041                     "modelVersion",
2042                     null,
2043                     "of '" + string + "' is newer than the versions supported by this version of Maven: "
2044                             + validVersions + ". Building this project requires a newer version of Maven.",
2045                     tracker);
2046 
2047         } else if (olderThanAll) {
2048             // note this will not be hit for Maven 1.x project.xml as it is an incompatible schema
2049             addViolation(
2050                     problems,
2051                     Severity.FATAL,
2052                     Version.V20,
2053                     "modelVersion",
2054                     null,
2055                     "of '" + string + "' is older than the versions supported by this version of Maven: "
2056                             + validVersions + ". Building this project requires an older version of Maven.",
2057                     tracker);
2058 
2059         } else {
2060             addViolation(
2061                     problems,
2062                     Severity.ERROR,
2063                     Version.V20,
2064                     "modelVersion",
2065                     null,
2066                     "must be one of " + validVersions + " but is '" + string + "'.",
2067                     tracker);
2068         }
2069 
2070         return false;
2071     }
2072 
2073     /**
2074      * Compares two model versions.
2075      *
2076      * @param first the first version.
2077      * @param second the second version.
2078      * @return negative if the first version is newer than the second version, zero if they are the same or positive if
2079      * the second version is the newer.
2080      */
2081     private static int compareModelVersions(String first, String second) {
2082         // we use a dedicated comparator because we control our model version scheme.
2083         String[] firstSegments = first.split("\\.");
2084         String[] secondSegments = second.split("\\.");
2085         for (int i = 0; i < Math.max(firstSegments.length, secondSegments.length); i++) {
2086             int result = asLong(i, firstSegments).compareTo(asLong(i, secondSegments));
2087             if (result != 0) {
2088                 return result;
2089             }
2090         }
2091         return 0;
2092     }
2093 
2094     private static Long asLong(int index, String[] segments) {
2095         try {
2096             return Long.valueOf(index < segments.length ? segments[index] : "0");
2097         } catch (NumberFormatException e) {
2098             return 0L;
2099         }
2100     }
2101 
2102     @SuppressWarnings("checkstyle:parameternumber")
2103     private boolean validateBannedCharacters(
2104             String prefix,
2105             String fieldName,
2106             ModelProblemCollector problems,
2107             Severity severity,
2108             Version version,
2109             String string,
2110             @Nullable SourceHint sourceHint,
2111             InputLocationTracker tracker,
2112             String banned) {
2113         if (string != null) {
2114             for (int i = string.length() - 1; i >= 0; i--) {
2115                 if (banned.indexOf(string.charAt(i)) >= 0) {
2116                     addViolation(
2117                             problems,
2118                             severity,
2119                             version,
2120                             prefix + fieldName,
2121                             sourceHint,
2122                             "must not contain any of these characters " + banned + " but found " + string.charAt(i),
2123                             tracker);
2124                     return false;
2125                 }
2126             }
2127         }
2128 
2129         return true;
2130     }
2131 
2132     @SuppressWarnings("checkstyle:parameternumber")
2133     private boolean validateVersion(
2134             String prefix,
2135             String fieldName,
2136             ModelProblemCollector problems,
2137             Severity severity,
2138             Version version,
2139             String string,
2140             @Nullable SourceHint sourceHint,
2141             InputLocationTracker tracker) {
2142         if (string == null || string.isEmpty()) {
2143             return true;
2144         }
2145 
2146         if (hasExpression(string)) {
2147             addViolation(
2148                     problems,
2149                     severity,
2150                     version,
2151                     prefix + fieldName,
2152                     sourceHint,
2153                     "must be a valid version but is '" + string + "'.",
2154                     tracker);
2155             return false;
2156         }
2157 
2158         return validateBannedCharacters(
2159                 prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
2160     }
2161 
2162     private boolean validate20ProperSnapshotVersion(
2163             String fieldName,
2164             ModelProblemCollector problems,
2165             Severity severity,
2166             Version version,
2167             String string,
2168             @Nullable SourceHint sourceHint,
2169             InputLocationTracker tracker) {
2170         if (string == null || string.isEmpty()) {
2171             return true;
2172         }
2173 
2174         if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
2175             addViolation(
2176                     problems,
2177                     severity,
2178                     version,
2179                     fieldName,
2180                     sourceHint,
2181                     "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.",
2182                     tracker);
2183             return false;
2184         }
2185 
2186         return true;
2187     }
2188 
2189     private boolean validate20PluginVersion(
2190             String fieldName,
2191             ModelProblemCollector problems,
2192             String string,
2193             @Nullable SourceHint sourceHint,
2194             InputLocationTracker tracker,
2195             int validationLevel) {
2196         if (string == null) {
2197             addViolation(
2198                     problems,
2199                     Severity.WARNING,
2200                     ModelProblem.Version.V20,
2201                     fieldName,
2202                     sourceHint,
2203                     " is missing.",
2204                     tracker);
2205             return false;
2206         }
2207 
2208         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
2209 
2210         if (!validateVersion(EMPTY, fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
2211             return false;
2212         }
2213 
2214         if (string.isEmpty() || "RELEASE".equals(string) || "LATEST".equals(string)) {
2215             addViolation(
2216                     problems,
2217                     errOn30,
2218                     Version.V20,
2219                     fieldName,
2220                     sourceHint,
2221                     "must be a valid version but is '" + string + "'.",
2222                     tracker);
2223             return false;
2224         }
2225 
2226         return true;
2227     }
2228 
2229     private static void addViolation(
2230             ModelProblemCollector problems,
2231             Severity severity,
2232             Version version,
2233             String fieldName,
2234             @Nullable SourceHint sourceHint,
2235             String message,
2236             InputLocationTracker tracker) {
2237         StringBuilder buffer = new StringBuilder(256);
2238         buffer.append('\'').append(fieldName).append('\'');
2239 
2240         if (sourceHint != null) {
2241             String hint = sourceHint.get();
2242             if (hint != null) {
2243                 buffer.append(" for ").append(hint);
2244             }
2245         }
2246 
2247         buffer.append(' ').append(message);
2248 
2249         problems.add(severity, version, buffer.toString(), getLocation(fieldName, tracker));
2250     }
2251 
2252     private static InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
2253         InputLocation location = null;
2254 
2255         if (tracker != null) {
2256             if (fieldName != null) {
2257                 Object key = fieldName;
2258 
2259                 int idx = fieldName.lastIndexOf('.');
2260                 if (idx >= 0) {
2261                     fieldName = fieldName.substring(idx + 1);
2262                     key = fieldName;
2263                 }
2264 
2265                 if (fieldName.endsWith("]")) {
2266                     key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
2267                     try {
2268                         key = Integer.valueOf(key.toString());
2269                     } catch (NumberFormatException e) {
2270                         // use key as is
2271                     }
2272                 }
2273 
2274                 location = tracker.getLocation(key);
2275             }
2276 
2277             if (location == null) {
2278                 location = tracker.getLocation(EMPTY);
2279             }
2280         }
2281 
2282         return location;
2283     }
2284 
2285     private static boolean equals(String s1, String s2) {
2286         String c1 = s1 == null ? "" : s1.trim();
2287         String c2 = s2 == null ? "" : s2.trim();
2288         return c1.equals(c2);
2289     }
2290 
2291     private static Severity getSeverity(int validationLevel, int errorThreshold) {
2292         if (validationLevel < errorThreshold) {
2293             return Severity.WARNING;
2294         } else {
2295             return Severity.ERROR;
2296         }
2297     }
2298 
2299     private interface SourceHint extends Supplier<String> {
2300         static SourceHint xmlNodeInputLocation(XmlNode xmlNode) {
2301             return () ->
2302                     xmlNode.inputLocation() != null ? xmlNode.inputLocation().toString() : null;
2303         }
2304 
2305         static SourceHint gav(String gav) {
2306             return () -> gav; // GAV
2307         }
2308 
2309         static SourceHint dependencyManagementKey(Dependency dependency) {
2310             return () -> {
2311                 String hint;
2312                 if (dependency.getClassifier() == null
2313                         || dependency.getClassifier().isEmpty()) {
2314                     hint = "groupId=" + valueToValueString(dependency.getGroupId())
2315                             + ", artifactId=" + valueToValueString(dependency.getArtifactId())
2316                             + ", type=" + valueToValueString(dependency.getType());
2317                 } else {
2318                     hint = "groupId=" + valueToValueString(dependency.getGroupId())
2319                             + ", artifactId=" + valueToValueString(dependency.getArtifactId())
2320                             + ", classifier=" + valueToValueString(dependency.getClassifier())
2321                             + ", type=" + valueToValueString(dependency.getType());
2322                 }
2323                 return hint;
2324             };
2325         }
2326 
2327         private static String valueToValueString(String value) {
2328             return value == null ? "" : "'" + value + "'";
2329         }
2330 
2331         static SourceHint pluginKey(Plugin plugin) {
2332             return plugin::getKey;
2333         }
2334 
2335         static SourceHint repoId(Repository repository) {
2336             return repository::getId;
2337         }
2338 
2339         @Nullable
2340         static SourceHint resourceDirectory(Resource resource) {
2341             return resource::getDirectory;
2342         }
2343     }
2344 }