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