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.di.Inject;
43  import org.apache.maven.api.di.Named;
44  import org.apache.maven.api.di.Singleton;
45  import org.apache.maven.api.model.Activation;
46  import org.apache.maven.api.model.ActivationFile;
47  import org.apache.maven.api.model.ActivationOS;
48  import org.apache.maven.api.model.ActivationProperty;
49  import org.apache.maven.api.model.Build;
50  import org.apache.maven.api.model.BuildBase;
51  import org.apache.maven.api.model.Dependency;
52  import org.apache.maven.api.model.DependencyManagement;
53  import org.apache.maven.api.model.DistributionManagement;
54  import org.apache.maven.api.model.Exclusion;
55  import org.apache.maven.api.model.InputLocation;
56  import org.apache.maven.api.model.InputLocationTracker;
57  import org.apache.maven.api.model.Model;
58  import org.apache.maven.api.model.Parent;
59  import org.apache.maven.api.model.Plugin;
60  import org.apache.maven.api.model.PluginExecution;
61  import org.apache.maven.api.model.PluginManagement;
62  import org.apache.maven.api.model.Profile;
63  import org.apache.maven.api.model.ReportPlugin;
64  import org.apache.maven.api.model.Reporting;
65  import org.apache.maven.api.model.Repository;
66  import org.apache.maven.api.model.Resource;
67  import org.apache.maven.api.services.BuilderProblem.Severity;
68  import org.apache.maven.api.services.ModelBuilder;
69  import org.apache.maven.api.services.ModelProblem;
70  import org.apache.maven.api.services.ModelProblem.Version;
71  import org.apache.maven.api.services.ModelProblemCollector;
72  import org.apache.maven.api.services.model.ModelValidator;
73  import org.apache.maven.api.xml.XmlNode;
74  import org.apache.maven.model.v4.MavenModelVersion;
75  import org.apache.maven.model.v4.MavenTransformer;
76  
77  /**
78   */
79  @Named
80  @Singleton
81  public class DefaultModelValidator implements ModelValidator {
82      public static final String BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION =
83              "maven.build.allowExpressionInEffectiveProjectVersion";
84  
85      public static final List<String> VALID_MODEL_VERSIONS = ModelBuilder.VALID_MODEL_VERSIONS;
86  
87      private static final Pattern EXPRESSION_NAME_PATTERN = Pattern.compile("\\$\\{(.+?)}");
88      private static final Pattern EXPRESSION_PROJECT_NAME_PATTERN = Pattern.compile("\\$\\{(project.+?)}");
89  
90      private static final String ILLEGAL_FS_CHARS = "\\/:\"<>|?*";
91  
92      private static final String ILLEGAL_VERSION_CHARS = ILLEGAL_FS_CHARS;
93  
94      private static final String ILLEGAL_REPO_ID_CHARS = ILLEGAL_FS_CHARS;
95  
96      private static final String EMPTY = "";
97  
98      private record ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {}
99  
100     private static class ActivationWalker extends MavenTransformer {
101 
102         private final Deque<ActivationFrame> stk;
103 
104         ActivationWalker(Deque<ActivationFrame> stk, UnaryOperator<String> transformer) {
105             super(transformer);
106             this.stk = stk;
107         }
108 
109         private ActivationFrame nextFrame(String property) {
110             return new ActivationFrame(property, Optional.empty());
111         }
112 
113         private <P> ActivationFrame nextFrame(String property, Function<P, InputLocationTracker> child) {
114             @SuppressWarnings("unchecked")
115             final Optional<P> parent = (Optional<P>) stk.peek().parent;
116             return new ActivationFrame(property, parent.map(child));
117         }
118 
119         @Override
120         public Activation transformActivation(Activation target) {
121             stk.push(new ActivationFrame("activation", Optional.of(target)));
122             try {
123                 return super.transformActivation(target);
124             } finally {
125                 stk.pop();
126             }
127         }
128 
129         @Override
130         protected Activation.Builder transformActivation_ActiveByDefault(
131                 Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
132             return builder;
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(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                     && VALID_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, VALID_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(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.getChildren()) {
790             validateXmlNodeRecursively(problems, fieldPathPrefix + "." + xmlNode.getName(), tracker, child);
791         }
792     }
793 
794     private void validateXmlNode(
795             ModelProblemCollector problems, String fieldPathPrefix, InputLocationTracker tracker, XmlNode xmlNode) {
796         String childrenCombinationModeAttribute = xmlNode.getAttributes()
797                 .getOrDefault(XmlNode.CHILDREN_COMBINATION_MODE_ATTRIBUTE, XmlNode.DEFAULT_CHILDREN_COMBINATION_MODE);
798         if (!(XmlNode.CHILDREN_COMBINATION_APPEND.equals(childrenCombinationModeAttribute)
799                 || XmlNode.CHILDREN_COMBINATION_MERGE.equals(childrenCombinationModeAttribute))) {
800             addViolation(
801                     problems,
802                     Severity.ERROR,
803                     Version.V40,
804                     fieldPathPrefix + "." + xmlNode.getName(),
805                     xmlNode.getInputLocation() != null
806                             ? xmlNode.getInputLocation().toString()
807                             : null,
808                     "Unsupported value '" + childrenCombinationModeAttribute + "' for "
809                             + XmlNode.CHILDREN_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
810                             + XmlNode.CHILDREN_COMBINATION_APPEND + ", and " + XmlNode.CHILDREN_COMBINATION_MERGE
811                             + " (default is: " + XmlNode.DEFAULT_SELF_COMBINATION_MODE + ")",
812                     tracker);
813         }
814         String selfCombinationModeAttribute = xmlNode.getAttributes()
815                 .getOrDefault(XmlNode.SELF_COMBINATION_MODE_ATTRIBUTE, XmlNode.DEFAULT_SELF_COMBINATION_MODE);
816         if (!(XmlNode.SELF_COMBINATION_OVERRIDE.equals(selfCombinationModeAttribute)
817                 || XmlNode.SELF_COMBINATION_MERGE.equals(selfCombinationModeAttribute)
818                 || XmlNode.SELF_COMBINATION_REMOVE.equals(selfCombinationModeAttribute))) {
819             addViolation(
820                     problems,
821                     Severity.ERROR,
822                     Version.V40,
823                     fieldPathPrefix + "." + xmlNode.getName(),
824                     xmlNode.getInputLocation() != null
825                             ? xmlNode.getInputLocation().toString()
826                             : null,
827                     "Unsupported value '" + selfCombinationModeAttribute + "' for "
828                             + XmlNode.SELF_COMBINATION_MODE_ATTRIBUTE + " attribute. " + "Valid values are: "
829                             + XmlNode.SELF_COMBINATION_OVERRIDE + ", " + XmlNode.SELF_COMBINATION_MERGE + ", and "
830                             + XmlNode.SELF_COMBINATION_REMOVE
831                             + " (default is: " + XmlNode.DEFAULT_SELF_COMBINATION_MODE + ")",
832                     tracker);
833         }
834     }
835 
836     @Override
837     @SuppressWarnings("checkstyle:MethodLength")
838     public void validateEffectiveModel(Model m, int validationLevel, ModelProblemCollector problems) {
839         validateStringNotEmpty("modelVersion", problems, Severity.ERROR, Version.BASE, m.getModelVersion(), m);
840 
841         validateCoordinatesId("groupId", problems, m.getGroupId(), m);
842 
843         validateCoordinatesId("artifactId", problems, m.getArtifactId(), m);
844 
845         validateStringNotEmpty("packaging", problems, Severity.ERROR, Version.BASE, m.getPackaging(), m);
846 
847         if (!m.getModules().isEmpty()) {
848             if (!"pom".equals(m.getPackaging())) {
849                 addViolation(
850                         problems,
851                         Severity.ERROR,
852                         Version.BASE,
853                         "packaging",
854                         null,
855                         "with value '" + m.getPackaging() + "' is invalid. Aggregator projects "
856                                 + "require 'pom' as packaging.",
857                         m);
858             }
859 
860             for (int i = 0, n = m.getModules().size(); i < n; i++) {
861                 String module = m.getModules().get(i);
862 
863                 boolean isBlankModule = true;
864                 if (module != null) {
865                     for (int j = 0; j < module.length(); j++) {
866                         if (!Character.isWhitespace(module.charAt(j))) {
867                             isBlankModule = false;
868                         }
869                     }
870                 }
871 
872                 if (isBlankModule) {
873                     addViolation(
874                             problems,
875                             Severity.ERROR,
876                             Version.BASE,
877                             "modules.module[" + i + "]",
878                             null,
879                             "has been specified without a path to the project directory.",
880                             m.getLocation("modules"));
881                 }
882             }
883         }
884 
885         validateStringNotEmpty("version", problems, Severity.ERROR, Version.BASE, m.getVersion(), m);
886 
887         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
888 
889         validateEffectiveDependencies(problems, m, m.getDependencies(), false, validationLevel);
890 
891         DependencyManagement mgmt = m.getDependencyManagement();
892         if (mgmt != null) {
893             validateEffectiveDependencies(problems, m, mgmt.getDependencies(), true, validationLevel);
894         }
895 
896         if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
897             Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
898 
899             validateBannedCharacters(
900                     EMPTY, "version", problems, errOn31, Version.V20, m.getVersion(), null, m, ILLEGAL_VERSION_CHARS);
901             validate20ProperSnapshotVersion("version", problems, errOn31, Version.V20, m.getVersion(), null, m);
902             if (hasExpression(m.getVersion())) {
903                 Severity versionExpressionSeverity = Severity.ERROR;
904                 if (m.getProperties() != null
905                         && Boolean.parseBoolean(
906                                 m.getProperties().get(BUILD_ALLOW_EXPRESSION_IN_EFFECTIVE_PROJECT_VERSION))) {
907                     versionExpressionSeverity = Severity.WARNING;
908                 }
909                 addViolation(
910                         problems,
911                         versionExpressionSeverity,
912                         Version.V20,
913                         "version",
914                         null,
915                         "must be a constant version but is '" + m.getVersion() + "'.",
916                         m);
917             }
918 
919             Build build = m.getBuild();
920             if (build != null) {
921                 for (Plugin p : build.getPlugins()) {
922                     validateStringNotEmpty(
923                             "build.plugins.plugin.artifactId",
924                             problems,
925                             Severity.ERROR,
926                             Version.V20,
927                             p.getArtifactId(),
928                             p);
929 
930                     validateStringNotEmpty(
931                             "build.plugins.plugin.groupId", problems, Severity.ERROR, Version.V20, p.getGroupId(), p);
932 
933                     validate20PluginVersion(
934                             "build.plugins.plugin.version", problems, p.getVersion(), p.getKey(), p, validationLevel);
935 
936                     validateBoolean(
937                             "build.plugins.plugin.inherited",
938                             EMPTY,
939                             problems,
940                             errOn30,
941                             Version.V20,
942                             p.getInherited(),
943                             p.getKey(),
944                             p);
945 
946                     validateBoolean(
947                             "build.plugins.plugin.extensions",
948                             EMPTY,
949                             problems,
950                             errOn30,
951                             Version.V20,
952                             p.getExtensions(),
953                             p.getKey(),
954                             p);
955 
956                     validate20EffectivePluginDependencies(problems, p, validationLevel);
957                 }
958 
959                 validate20RawResources(problems, build.getResources(), "build.resources.resource.", validationLevel);
960 
961                 validate20RawResources(
962                         problems, build.getTestResources(), "build.testResources.testResource.", validationLevel);
963             }
964 
965             Reporting reporting = m.getReporting();
966             if (reporting != null) {
967                 for (ReportPlugin p : reporting.getPlugins()) {
968                     validateStringNotEmpty(
969                             "reporting.plugins.plugin.artifactId",
970                             problems,
971                             Severity.ERROR,
972                             Version.V20,
973                             p.getArtifactId(),
974                             p);
975 
976                     validateStringNotEmpty(
977                             "reporting.plugins.plugin.groupId",
978                             problems,
979                             Severity.ERROR,
980                             Version.V20,
981                             p.getGroupId(),
982                             p);
983                 }
984             }
985 
986             for (Repository repository : m.getRepositories()) {
987                 validate20EffectiveRepository(problems, repository, "repositories.repository.", validationLevel);
988             }
989 
990             for (Repository repository : m.getPluginRepositories()) {
991                 validate20EffectiveRepository(
992                         problems, repository, "pluginRepositories.pluginRepository.", validationLevel);
993             }
994 
995             DistributionManagement distMgmt = m.getDistributionManagement();
996             if (distMgmt != null) {
997                 if (distMgmt.getStatus() != null) {
998                     addViolation(
999                             problems,
1000                             Severity.ERROR,
1001                             Version.V20,
1002                             "distributionManagement.status",
1003                             null,
1004                             "must not be specified.",
1005                             distMgmt);
1006                 }
1007 
1008                 validate20EffectiveRepository(
1009                         problems, distMgmt.getRepository(), "distributionManagement.repository.", validationLevel);
1010                 validate20EffectiveRepository(
1011                         problems,
1012                         distMgmt.getSnapshotRepository(),
1013                         "distributionManagement.snapshotRepository.",
1014                         validationLevel);
1015             }
1016         }
1017     }
1018 
1019     private void validate20RawDependencies(
1020             ModelProblemCollector problems,
1021             List<Dependency> dependencies,
1022             String prefix,
1023             String prefix2,
1024             boolean is41OrBeyond,
1025             int validationLevel) {
1026         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1027         Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
1028 
1029         Map<String, Dependency> index = new HashMap<>();
1030 
1031         for (Dependency dependency : dependencies) {
1032             String key = dependency.getManagementKey();
1033 
1034             if ("import".equals(dependency.getScope())) {
1035                 if (!"pom".equals(dependency.getType())) {
1036                     addViolation(
1037                             problems,
1038                             Severity.WARNING,
1039                             Version.V20,
1040                             prefix + prefix2 + "type",
1041                             key,
1042                             "must be 'pom' to import the managed dependencies.",
1043                             dependency);
1044                 } else if (!is41OrBeyond
1045                         && dependency.getClassifier() != null
1046                         && !dependency.getClassifier().isEmpty()) {
1047                     addViolation(
1048                             problems,
1049                             errOn30,
1050                             Version.V20,
1051                             prefix + prefix2 + "classifier",
1052                             key,
1053                             "must be empty, imported POM cannot have a classifier.",
1054                             dependency);
1055                 }
1056             } else if ("system".equals(dependency.getScope())) {
1057 
1058                 if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_3_1) {
1059                     addViolation(
1060                             problems,
1061                             Severity.WARNING,
1062                             Version.V31,
1063                             prefix + prefix2 + "scope",
1064                             key,
1065                             "declares usage of deprecated 'system' scope ",
1066                             dependency);
1067                 }
1068 
1069                 String sysPath = dependency.getSystemPath();
1070                 if (sysPath != null && !sysPath.isEmpty()) {
1071                     if (!hasExpression(sysPath)) {
1072                         addViolation(
1073                                 problems,
1074                                 Severity.WARNING,
1075                                 Version.V20,
1076                                 prefix + prefix2 + "systemPath",
1077                                 key,
1078                                 "should use a variable instead of a hard-coded path " + sysPath,
1079                                 dependency);
1080                     } else if (sysPath.contains("${basedir}") || sysPath.contains("${project.basedir}")) {
1081                         addViolation(
1082                                 problems,
1083                                 Severity.WARNING,
1084                                 Version.V20,
1085                                 prefix + prefix2 + "systemPath",
1086                                 key,
1087                                 "should not point at files within the project directory, " + sysPath
1088                                         + " will be unresolvable by dependent projects",
1089                                 dependency);
1090                     }
1091                 }
1092             }
1093 
1094             if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) {
1095                 addViolation(
1096                         problems,
1097                         Severity.WARNING,
1098                         Version.BASE,
1099                         prefix + prefix2 + "version",
1100                         key,
1101                         "is either LATEST or RELEASE (both of them are being deprecated)",
1102                         dependency);
1103             }
1104 
1105             Dependency existing = index.get(key);
1106 
1107             if (existing != null) {
1108                 String msg;
1109                 if (equals(existing.getVersion(), dependency.getVersion())) {
1110                     msg = "duplicate declaration of version " + Objects.toString(dependency.getVersion(), "(?)");
1111                 } else {
1112                     msg = "version " + Objects.toString(existing.getVersion(), "(?)") + " vs "
1113                             + Objects.toString(dependency.getVersion(), "(?)");
1114                 }
1115 
1116                 addViolation(
1117                         problems,
1118                         errOn31,
1119                         Version.V20,
1120                         prefix + prefix2 + "(groupId:artifactId:type:classifier)",
1121                         null,
1122                         "must be unique: " + key + " -> " + msg,
1123                         dependency);
1124             } else {
1125                 index.put(key, dependency);
1126             }
1127         }
1128     }
1129 
1130     private void validate20RawDependenciesSelfReferencing(
1131             ModelProblemCollector problems, Model m, List<Dependency> dependencies, String prefix) {
1132         // We only check for groupId/artifactId/version/classifier cause if there is another
1133         // module with the same groupId/artifactId/version/classifier this will fail the build
1134         // earlier like "Project '...' is duplicated in the reactor.
1135         // So it is sufficient to check only groupId/artifactId/version/classifier and not the
1136         // packaging type.
1137         for (Dependency dependency : dependencies) {
1138             String key = dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getVersion()
1139                     + (dependency.getClassifier() != null ? ":" + dependency.getClassifier() : EMPTY);
1140             String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
1141             if (key.equals(mKey)) {
1142                 // This means a module which is build has a dependency which has the same
1143                 // groupId, artifactId, version and classifier coordinates. This is in consequence
1144                 // a self reference or in other words a circular reference which can not being resolved.
1145                 addViolation(
1146                         problems,
1147                         Severity.FATAL,
1148                         Version.V31,
1149                         prefix + "[" + key + "]",
1150                         key,
1151                         "is referencing itself.",
1152                         dependency);
1153             }
1154         }
1155     }
1156 
1157     private void validateEffectiveDependencies(
1158             ModelProblemCollector problems,
1159             Model m,
1160             List<Dependency> dependencies,
1161             boolean management,
1162             int validationLevel) {
1163         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1164 
1165         String prefix = management ? "dependencyManagement.dependencies.dependency." : "dependencies.dependency.";
1166 
1167         for (Dependency d : dependencies) {
1168             validateEffectiveDependency(problems, d, management, prefix, validationLevel);
1169 
1170             if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
1171                 validateBoolean(
1172                         prefix, "optional", problems, errOn30, Version.V20, d.getOptional(), d.getManagementKey(), d);
1173 
1174                 if (!management) {
1175                     validateVersion(
1176                             prefix, "version", problems, errOn30, Version.V20, d.getVersion(), d.getManagementKey(), d);
1177 
1178                     /*
1179                      * TODO Extensions like Flex Mojos use custom scopes like "merged", "internal", "external", etc. In
1180                      * order to don't break backward-compat with those, only warn but don't error out.
1181                      */
1182                     validateEnum(
1183                             prefix,
1184                             "scope",
1185                             problems,
1186                             Severity.WARNING,
1187                             Version.V20,
1188                             d.getScope(),
1189                             d.getManagementKey(),
1190                             d,
1191                             "provided",
1192                             "compile",
1193                             "runtime",
1194                             "test",
1195                             "system");
1196 
1197                     validateEffectiveModelAgainstDependency(prefix, problems, m, d);
1198                 } else {
1199                     validateEnum(
1200                             prefix,
1201                             "scope",
1202                             problems,
1203                             Severity.WARNING,
1204                             Version.V20,
1205                             d.getScope(),
1206                             d.getManagementKey(),
1207                             d,
1208                             "provided",
1209                             "compile",
1210                             "runtime",
1211                             "test",
1212                             "system",
1213                             "import");
1214                 }
1215             }
1216         }
1217     }
1218 
1219     private void validateEffectiveModelAgainstDependency(
1220             String prefix, ModelProblemCollector problems, Model m, Dependency d) {
1221         String key = d.getGroupId() + ":" + d.getArtifactId() + ":" + d.getVersion()
1222                 + (d.getClassifier() != null ? ":" + d.getClassifier() : EMPTY);
1223         String mKey = m.getGroupId() + ":" + m.getArtifactId() + ":" + m.getVersion();
1224         if (key.equals(mKey)) {
1225             // This means a module which is build has a dependency which has the same
1226             // groupId, artifactId, version and classifier coordinates. This is in consequence
1227             // a self reference or in other words a circular reference which can not being resolved.
1228             addViolation(
1229                     problems, Severity.FATAL, Version.V31, prefix + "[" + key + "]", key, "is referencing itself.", d);
1230         }
1231     }
1232 
1233     private void validate20EffectivePluginDependencies(
1234             ModelProblemCollector problems, Plugin plugin, int validationLevel) {
1235         List<Dependency> dependencies = plugin.getDependencies();
1236 
1237         if (!dependencies.isEmpty()) {
1238             String prefix = "build.plugins.plugin[" + plugin.getKey() + "].dependencies.dependency.";
1239 
1240             Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1241 
1242             for (Dependency d : dependencies) {
1243                 validateEffectiveDependency(problems, d, false, prefix, validationLevel);
1244 
1245                 validateVersion(
1246                         prefix, "version", problems, errOn30, Version.BASE, d.getVersion(), d.getManagementKey(), d);
1247 
1248                 validateEnum(
1249                         prefix,
1250                         "scope",
1251                         problems,
1252                         errOn30,
1253                         Version.BASE,
1254                         d.getScope(),
1255                         d.getManagementKey(),
1256                         d,
1257                         "compile",
1258                         "runtime",
1259                         "system");
1260             }
1261         }
1262     }
1263 
1264     private void validateEffectiveDependency(
1265             ModelProblemCollector problems, Dependency d, boolean management, String prefix, int validationLevel) {
1266         validateCoordinatesId(
1267                 prefix,
1268                 "artifactId",
1269                 problems,
1270                 Severity.ERROR,
1271                 Version.BASE,
1272                 d.getArtifactId(),
1273                 d.getManagementKey(),
1274                 d);
1275 
1276         validateCoordinatesId(
1277                 prefix, "groupId", problems, Severity.ERROR, Version.BASE, d.getGroupId(), d.getManagementKey(), d);
1278 
1279         if (!management) {
1280             validateStringNotEmpty(
1281                     prefix, "type", problems, Severity.ERROR, Version.BASE, d.getType(), d.getManagementKey(), d);
1282 
1283             validateDependencyVersion(problems, d, prefix);
1284         }
1285 
1286         if ("system".equals(d.getScope())) {
1287             String systemPath = d.getSystemPath();
1288 
1289             if (systemPath == null || systemPath.isEmpty()) {
1290                 addViolation(
1291                         problems,
1292                         Severity.ERROR,
1293                         Version.BASE,
1294                         prefix + "systemPath",
1295                         d.getManagementKey(),
1296                         "is missing.",
1297                         d);
1298             } else {
1299                 File sysFile = new File(systemPath);
1300                 if (!sysFile.isAbsolute()) {
1301                     addViolation(
1302                             problems,
1303                             Severity.ERROR,
1304                             Version.BASE,
1305                             prefix + "systemPath",
1306                             d.getManagementKey(),
1307                             "must specify an absolute path but is " + systemPath,
1308                             d);
1309                 } else if (!sysFile.isFile()) {
1310                     String msg = "refers to a non-existing file " + sysFile.getAbsolutePath() + ".";
1311                     addViolation(
1312                             problems,
1313                             Severity.WARNING,
1314                             Version.BASE,
1315                             prefix + "systemPath",
1316                             d.getManagementKey(),
1317                             msg,
1318                             d);
1319                 }
1320             }
1321         } else if (d.getSystemPath() != null && !d.getSystemPath().isEmpty()) {
1322             addViolation(
1323                     problems,
1324                     Severity.ERROR,
1325                     Version.BASE,
1326                     prefix + "systemPath",
1327                     d.getManagementKey(),
1328                     "must be omitted. This field may only be specified for a dependency with system scope.",
1329                     d);
1330         }
1331 
1332         if (validationLevel >= ModelValidator.VALIDATION_LEVEL_MAVEN_2_0) {
1333             for (Exclusion exclusion : d.getExclusions()) {
1334                 if (validationLevel < ModelValidator.VALIDATION_LEVEL_MAVEN_3_0) {
1335                     validateCoordinatesId(
1336                             prefix,
1337                             "exclusions.exclusion.groupId",
1338                             problems,
1339                             Severity.WARNING,
1340                             Version.V20,
1341                             exclusion.getGroupId(),
1342                             d.getManagementKey(),
1343                             exclusion);
1344 
1345                     validateCoordinatesId(
1346                             prefix,
1347                             "exclusions.exclusion.artifactId",
1348                             problems,
1349                             Severity.WARNING,
1350                             Version.V20,
1351                             exclusion.getArtifactId(),
1352                             d.getManagementKey(),
1353                             exclusion);
1354                 } else {
1355                     validateCoordinatesIdWithWildcards(
1356                             prefix,
1357                             "exclusions.exclusion.groupId",
1358                             problems,
1359                             Severity.WARNING,
1360                             Version.V30,
1361                             exclusion.getGroupId(),
1362                             d.getManagementKey(),
1363                             exclusion);
1364 
1365                     validateCoordinatesIdWithWildcards(
1366                             prefix,
1367                             "exclusions.exclusion.artifactId",
1368                             problems,
1369                             Severity.WARNING,
1370                             Version.V30,
1371                             exclusion.getArtifactId(),
1372                             d.getManagementKey(),
1373                             exclusion);
1374                 }
1375             }
1376         }
1377     }
1378 
1379     /**
1380      * @since 3.2.4
1381      */
1382     protected void validateDependencyVersion(ModelProblemCollector problems, Dependency d, String prefix) {
1383         validateStringNotEmpty(
1384                 prefix, "version", problems, Severity.ERROR, Version.BASE, d.getVersion(), d.getManagementKey(), d);
1385     }
1386 
1387     private void validateRawRepositories(
1388             ModelProblemCollector problems,
1389             List<Repository> repositories,
1390             String prefix,
1391             String prefix2,
1392             int validationLevel) {
1393         Map<String, Repository> index = new HashMap<>();
1394 
1395         for (Repository repository : repositories) {
1396             validateStringNotEmpty(
1397                     prefix, prefix2, "id", problems, Severity.ERROR, Version.V20, repository.getId(), null, repository);
1398 
1399             if (validateStringNotEmpty(
1400                     prefix,
1401                     prefix2,
1402                     "[" + repository.getId() + "].url",
1403                     problems,
1404                     Severity.ERROR,
1405                     Version.V20,
1406                     repository.getUrl(),
1407                     null,
1408                     repository)) {
1409                 // only allow ${basedir} and ${project.basedir}
1410                 Matcher m = EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
1411                 while (m.find()) {
1412                     String expr = m.group(1);
1413                     if (!("basedir".equals(expr)
1414                             || "project.basedir".equals(expr)
1415                             || expr.startsWith("project.basedir.")
1416                             || "project.rootDirectory".equals(expr)
1417                             || expr.startsWith("project.rootDirectory."))) {
1418                         addViolation(
1419                                 problems,
1420                                 Severity.ERROR,
1421                                 Version.V40,
1422                                 prefix + prefix2 + "[" + repository.getId() + "].url",
1423                                 null,
1424                                 "contains an unsupported expression (only expressions starting with 'project.basedir' or 'project.rootDirectory' are supported).",
1425                                 repository);
1426                         break;
1427                     }
1428                 }
1429             }
1430 
1431             String key = repository.getId();
1432 
1433             Repository existing = index.get(key);
1434 
1435             if (existing != null) {
1436                 Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1437 
1438                 addViolation(
1439                         problems,
1440                         errOn30,
1441                         Version.V20,
1442                         prefix + prefix2 + "id",
1443                         null,
1444                         "must be unique: " + repository.getId() + " -> " + existing.getUrl() + " vs "
1445                                 + repository.getUrl(),
1446                         repository);
1447             } else {
1448                 index.put(key, repository);
1449             }
1450         }
1451     }
1452 
1453     private void validate20EffectiveRepository(
1454             ModelProblemCollector problems, Repository repository, String prefix, int validationLevel) {
1455         if (repository != null) {
1456             Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1);
1457 
1458             validateBannedCharacters(
1459                     prefix,
1460                     "id",
1461                     problems,
1462                     errOn31,
1463                     Version.V20,
1464                     repository.getId(),
1465                     null,
1466                     repository,
1467                     ILLEGAL_REPO_ID_CHARS);
1468 
1469             if ("local".equals(repository.getId())) {
1470                 addViolation(
1471                         problems,
1472                         errOn31,
1473                         Version.V20,
1474                         prefix + "id",
1475                         null,
1476                         "must not be 'local'" + ", this identifier is reserved for the local repository"
1477                                 + ", using it for other repositories will corrupt your repository metadata.",
1478                         repository);
1479             }
1480 
1481             if ("legacy".equals(repository.getLayout())) {
1482                 addViolation(
1483                         problems,
1484                         Severity.WARNING,
1485                         Version.V20,
1486                         prefix + "layout",
1487                         repository.getId(),
1488                         "uses the unsupported value 'legacy', artifact resolution might fail.",
1489                         repository);
1490             }
1491         }
1492     }
1493 
1494     private void validate20RawResources(
1495             ModelProblemCollector problems, List<Resource> resources, String prefix, int validationLevel) {
1496         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
1497 
1498         for (Resource resource : resources) {
1499             validateStringNotEmpty(
1500                     prefix,
1501                     "directory",
1502                     problems,
1503                     Severity.ERROR,
1504                     Version.V20,
1505                     resource.getDirectory(),
1506                     null,
1507                     resource);
1508 
1509             validateBoolean(
1510                     prefix,
1511                     "filtering",
1512                     problems,
1513                     errOn30,
1514                     Version.V20,
1515                     resource.getFiltering(),
1516                     resource.getDirectory(),
1517                     resource);
1518         }
1519     }
1520 
1521     // ----------------------------------------------------------------------
1522     // Field validation
1523     // ----------------------------------------------------------------------
1524 
1525     private boolean validateCoordinatesId(
1526             String fieldName, ModelProblemCollector problems, String id, InputLocationTracker tracker) {
1527         return validateCoordinatesId(EMPTY, fieldName, problems, Severity.ERROR, Version.BASE, id, null, tracker);
1528     }
1529 
1530     @SuppressWarnings("checkstyle:parameternumber")
1531     private boolean validateCoordinatesId(
1532             String prefix,
1533             String fieldName,
1534             ModelProblemCollector problems,
1535             Severity severity,
1536             Version version,
1537             String id,
1538             String sourceHint,
1539             InputLocationTracker tracker) {
1540         if (id != null && validCoordinatesIds.contains(id)) {
1541             return true;
1542         }
1543         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1544             return false;
1545         } else {
1546             if (!isValidCoordinatesId(id)) {
1547                 addViolation(
1548                         problems,
1549                         severity,
1550                         version,
1551                         prefix + fieldName,
1552                         sourceHint,
1553                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1554                         tracker);
1555                 return false;
1556             }
1557             validCoordinatesIds.add(id);
1558             return true;
1559         }
1560     }
1561 
1562     private boolean isValidCoordinatesId(String id) {
1563         for (int i = 0; i < id.length(); i++) {
1564             char c = id.charAt(i);
1565             if (!isValidCoordinatesIdCharacter(c)) {
1566                 return false;
1567             }
1568         }
1569         return true;
1570     }
1571 
1572     private boolean isValidCoordinatesIdCharacter(char c) {
1573         return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.';
1574     }
1575 
1576     @SuppressWarnings("checkstyle:parameternumber")
1577     private boolean validateProfileId(
1578             String prefix,
1579             String fieldName,
1580             ModelProblemCollector problems,
1581             Severity severity,
1582             Version version,
1583             String id,
1584             String sourceHint,
1585             InputLocationTracker tracker) {
1586         if (validProfileIds.contains(id)) {
1587             return true;
1588         }
1589         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1590             return false;
1591         } else {
1592             if (!isValidProfileId(id)) {
1593                 addViolation(
1594                         problems,
1595                         severity,
1596                         version,
1597                         prefix + fieldName,
1598                         sourceHint,
1599                         "with value '" + id + "' does not match a valid profile id pattern.",
1600                         tracker);
1601                 return false;
1602             }
1603             validProfileIds.add(id);
1604             return true;
1605         }
1606     }
1607 
1608     private boolean isValidProfileId(String id) {
1609         return switch (id.charAt(0)) { // avoid first character that has special CLI meaning in "mvn -P xxx"
1610                 // +: activate
1611                 // -, !: deactivate
1612                 // ?: optional
1613             case '+', '-', '!', '?' -> false;
1614             default -> true;
1615         };
1616     }
1617 
1618     @SuppressWarnings("checkstyle:parameternumber")
1619     private boolean validateCoordinatesIdWithWildcards(
1620             String prefix,
1621             String fieldName,
1622             ModelProblemCollector problems,
1623             Severity severity,
1624             Version version,
1625             String id,
1626             String sourceHint,
1627             InputLocationTracker tracker) {
1628         if (!validateStringNotEmpty(prefix, fieldName, problems, severity, version, id, sourceHint, tracker)) {
1629             return false;
1630         } else {
1631             if (!isValidCoordinatesIdWithWildCards(id)) {
1632                 addViolation(
1633                         problems,
1634                         severity,
1635                         version,
1636                         prefix + fieldName,
1637                         sourceHint,
1638                         "with value '" + id + "' does not match a valid coordinate id pattern.",
1639                         tracker);
1640                 return false;
1641             }
1642             return true;
1643         }
1644     }
1645 
1646     private boolean isValidCoordinatesIdWithWildCards(String id) {
1647         for (int i = 0; i < id.length(); i++) {
1648             char c = id.charAt(i);
1649             if (!isValidCoordinatesIdWithWildCardCharacter(c)) {
1650                 return false;
1651             }
1652         }
1653         return true;
1654     }
1655 
1656     private boolean isValidCoordinatesIdWithWildCardCharacter(char c) {
1657         return isValidCoordinatesIdCharacter(c) || c == '?' || c == '*';
1658     }
1659 
1660     private boolean validateStringNoExpression(
1661             String fieldName,
1662             ModelProblemCollector problems,
1663             Severity severity,
1664             Version version,
1665             String string,
1666             InputLocationTracker tracker) {
1667         if (!hasExpression(string)) {
1668             return true;
1669         }
1670 
1671         addViolation(
1672                 problems,
1673                 severity,
1674                 version,
1675                 fieldName,
1676                 null,
1677                 "contains an expression but should be a constant.",
1678                 tracker);
1679 
1680         return false;
1681     }
1682 
1683     private boolean validateVersionNoExpression(
1684             String fieldName,
1685             ModelProblemCollector problems,
1686             Severity severity,
1687             Version version,
1688             String string,
1689             InputLocationTracker tracker) {
1690         if (!hasExpression(string)) {
1691             return true;
1692         }
1693 
1694         Matcher m = EXPRESSION_NAME_PATTERN.matcher(string.trim());
1695         if (m.find()) {
1696             addViolation(
1697                     problems,
1698                     severity,
1699                     version,
1700                     fieldName,
1701                     null,
1702                     "contains an expression but should be a constant.",
1703                     tracker);
1704         }
1705 
1706         return true;
1707     }
1708 
1709     private boolean hasExpression(String value) {
1710         return value != null && value.contains("${");
1711     }
1712 
1713     private boolean hasProjectExpression(String value) {
1714         return value != null && value.contains("${project.");
1715     }
1716 
1717     private boolean validateStringNotEmpty(
1718             String fieldName,
1719             ModelProblemCollector problems,
1720             Severity severity,
1721             Version version,
1722             String string,
1723             InputLocationTracker tracker) {
1724         return validateStringNotEmpty(EMPTY, fieldName, problems, severity, version, string, null, tracker);
1725     }
1726 
1727     /**
1728      * Asserts:
1729      * <p/>
1730      * <ul>
1731      * <li><code>string != null</code>
1732      * <li><code>string.length > 0</code>
1733      * </ul>
1734      */
1735     @SuppressWarnings("checkstyle:parameternumber")
1736     private boolean validateStringNotEmpty(
1737             String prefix,
1738             String prefix2,
1739             String fieldName,
1740             ModelProblemCollector problems,
1741             Severity severity,
1742             Version version,
1743             String string,
1744             String sourceHint,
1745             InputLocationTracker tracker) {
1746         if (!validateNotNull(prefix, prefix2, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1747             return false;
1748         }
1749 
1750         if (!string.isEmpty()) {
1751             return true;
1752         }
1753 
1754         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1755 
1756         return false;
1757     }
1758 
1759     /**
1760      * Asserts:
1761      * <p/>
1762      * <ul>
1763      * <li><code>string != null</code>
1764      * <li><code>string.length > 0</code>
1765      * </ul>
1766      */
1767     @SuppressWarnings("checkstyle:parameternumber")
1768     private boolean validateStringNotEmpty(
1769             String prefix,
1770             String fieldName,
1771             ModelProblemCollector problems,
1772             Severity severity,
1773             Version version,
1774             String string,
1775             String sourceHint,
1776             InputLocationTracker tracker) {
1777         if (!validateNotNull(prefix, fieldName, problems, severity, version, string, sourceHint, tracker)) {
1778             return false;
1779         }
1780 
1781         if (!string.isEmpty()) {
1782             return true;
1783         }
1784 
1785         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1786 
1787         return false;
1788     }
1789 
1790     /**
1791      * Asserts:
1792      * <p/>
1793      * <ul>
1794      * <li><code>string != null</code>
1795      * </ul>
1796      */
1797     @SuppressWarnings("checkstyle:parameternumber")
1798     private boolean validateNotNull(
1799             String prefix,
1800             String fieldName,
1801             ModelProblemCollector problems,
1802             Severity severity,
1803             Version version,
1804             Object object,
1805             String sourceHint,
1806             InputLocationTracker tracker) {
1807         if (object != null) {
1808             return true;
1809         }
1810 
1811         addViolation(problems, severity, version, prefix + fieldName, sourceHint, "is missing.", tracker);
1812 
1813         return false;
1814     }
1815 
1816     /**
1817      * Asserts:
1818      * <p/>
1819      * <ul>
1820      * <li><code>string != null</code>
1821      * </ul>
1822      */
1823     @SuppressWarnings("checkstyle:parameternumber")
1824     private boolean validateNotNull(
1825             String prefix,
1826             String prefix2,
1827             String fieldName,
1828             ModelProblemCollector problems,
1829             Severity severity,
1830             Version version,
1831             Object object,
1832             String sourceHint,
1833             InputLocationTracker tracker) {
1834         if (object != null) {
1835             return true;
1836         }
1837 
1838         addViolation(problems, severity, version, prefix + prefix2 + fieldName, sourceHint, "is missing.", tracker);
1839 
1840         return false;
1841     }
1842 
1843     @SuppressWarnings("checkstyle:parameternumber")
1844     private boolean validateBoolean(
1845             String prefix,
1846             String fieldName,
1847             ModelProblemCollector problems,
1848             Severity severity,
1849             Version version,
1850             String string,
1851             String sourceHint,
1852             InputLocationTracker tracker) {
1853         if (string == null || string.isEmpty()) {
1854             return true;
1855         }
1856 
1857         if ("true".equalsIgnoreCase(string) || "false".equalsIgnoreCase(string)) {
1858             return true;
1859         }
1860 
1861         addViolation(
1862                 problems,
1863                 severity,
1864                 version,
1865                 prefix + fieldName,
1866                 sourceHint,
1867                 "must be 'true' or 'false' but is '" + string + "'.",
1868                 tracker);
1869 
1870         return false;
1871     }
1872 
1873     @SuppressWarnings("checkstyle:parameternumber")
1874     private boolean validateEnum(
1875             String prefix,
1876             String fieldName,
1877             ModelProblemCollector problems,
1878             Severity severity,
1879             Version version,
1880             String string,
1881             String sourceHint,
1882             InputLocationTracker tracker,
1883             String... validValues) {
1884         if (string == null || string.isEmpty()) {
1885             return true;
1886         }
1887 
1888         List<String> values = Arrays.asList(validValues);
1889 
1890         if (values.contains(string)) {
1891             return true;
1892         }
1893 
1894         addViolation(
1895                 problems,
1896                 severity,
1897                 version,
1898                 prefix + fieldName,
1899                 sourceHint,
1900                 "must be one of " + values + " but is '" + string + "'.",
1901                 tracker);
1902 
1903         return false;
1904     }
1905 
1906     @SuppressWarnings("checkstyle:parameternumber")
1907     private boolean validateModelVersion(
1908             ModelProblemCollector problems, String string, InputLocationTracker tracker, List<String> validVersions) {
1909         if (string == null || string.isEmpty()) {
1910             return true;
1911         }
1912 
1913         if (validVersions.contains(string)) {
1914             return true;
1915         }
1916 
1917         boolean newerThanAll = true;
1918         boolean olderThanAll = true;
1919         for (String validValue : validVersions) {
1920             final int comparison = compareModelVersions(validValue, string);
1921             newerThanAll = newerThanAll && comparison < 0;
1922             olderThanAll = olderThanAll && comparison > 0;
1923         }
1924 
1925         if (newerThanAll) {
1926             addViolation(
1927                     problems,
1928                     Severity.FATAL,
1929                     Version.V20,
1930                     "modelVersion",
1931                     null,
1932                     "of '" + string + "' is newer than the versions supported by this version of Maven: "
1933                             + validVersions + ". Building this project requires a newer version of Maven.",
1934                     tracker);
1935 
1936         } else if (olderThanAll) {
1937             // note this will not be hit for Maven 1.x project.xml as it is an incompatible schema
1938             addViolation(
1939                     problems,
1940                     Severity.FATAL,
1941                     Version.V20,
1942                     "modelVersion",
1943                     null,
1944                     "of '" + string + "' is older than the versions supported by this version of Maven: "
1945                             + validVersions + ". Building this project requires an older version of Maven.",
1946                     tracker);
1947 
1948         } else {
1949             addViolation(
1950                     problems,
1951                     Severity.ERROR,
1952                     Version.V20,
1953                     "modelVersion",
1954                     null,
1955                     "must be one of " + validVersions + " but is '" + string + "'.",
1956                     tracker);
1957         }
1958 
1959         return false;
1960     }
1961 
1962     /**
1963      * Compares two model versions.
1964      *
1965      * @param first the first version.
1966      * @param second the second version.
1967      * @return negative if the first version is newer than the second version, zero if they are the same or positive if
1968      * the second version is the newer.
1969      */
1970     private static int compareModelVersions(String first, String second) {
1971         // we use a dedicated comparator because we control our model version scheme.
1972         String[] firstSegments = first.split("\\.");
1973         String[] secondSegments = second.split("\\.");
1974         for (int i = 0; i < Math.max(firstSegments.length, secondSegments.length); i++) {
1975             int result = asLong(i, firstSegments).compareTo(asLong(i, secondSegments));
1976             if (result != 0) {
1977                 return result;
1978             }
1979         }
1980         return 0;
1981     }
1982 
1983     private static Long asLong(int index, String[] segments) {
1984         try {
1985             return Long.valueOf(index < segments.length ? segments[index] : "0");
1986         } catch (NumberFormatException e) {
1987             return 0L;
1988         }
1989     }
1990 
1991     @SuppressWarnings("checkstyle:parameternumber")
1992     private boolean validateBannedCharacters(
1993             String prefix,
1994             String fieldName,
1995             ModelProblemCollector problems,
1996             Severity severity,
1997             Version version,
1998             String string,
1999             String sourceHint,
2000             InputLocationTracker tracker,
2001             String banned) {
2002         if (string != null) {
2003             for (int i = string.length() - 1; i >= 0; i--) {
2004                 if (banned.indexOf(string.charAt(i)) >= 0) {
2005                     addViolation(
2006                             problems,
2007                             severity,
2008                             version,
2009                             prefix + fieldName,
2010                             sourceHint,
2011                             "must not contain any of these characters " + banned + " but found " + string.charAt(i),
2012                             tracker);
2013                     return false;
2014                 }
2015             }
2016         }
2017 
2018         return true;
2019     }
2020 
2021     @SuppressWarnings("checkstyle:parameternumber")
2022     private boolean validateVersion(
2023             String prefix,
2024             String fieldName,
2025             ModelProblemCollector problems,
2026             Severity severity,
2027             Version version,
2028             String string,
2029             String sourceHint,
2030             InputLocationTracker tracker) {
2031         if (string == null || string.isEmpty()) {
2032             return true;
2033         }
2034 
2035         if (hasExpression(string)) {
2036             addViolation(
2037                     problems,
2038                     severity,
2039                     version,
2040                     prefix + fieldName,
2041                     sourceHint,
2042                     "must be a valid version but is '" + string + "'.",
2043                     tracker);
2044             return false;
2045         }
2046 
2047         return validateBannedCharacters(
2048                 prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
2049     }
2050 
2051     private boolean validate20ProperSnapshotVersion(
2052             String fieldName,
2053             ModelProblemCollector problems,
2054             Severity severity,
2055             Version version,
2056             String string,
2057             String sourceHint,
2058             InputLocationTracker tracker) {
2059         if (string == null || string.isEmpty()) {
2060             return true;
2061         }
2062 
2063         if (string.endsWith("SNAPSHOT") && !string.endsWith("-SNAPSHOT")) {
2064             addViolation(
2065                     problems,
2066                     severity,
2067                     version,
2068                     fieldName,
2069                     sourceHint,
2070                     "uses an unsupported snapshot version format, should be '*-SNAPSHOT' instead.",
2071                     tracker);
2072             return false;
2073         }
2074 
2075         return true;
2076     }
2077 
2078     private boolean validate20PluginVersion(
2079             String fieldName,
2080             ModelProblemCollector problems,
2081             String string,
2082             String sourceHint,
2083             InputLocationTracker tracker,
2084             int validationLevel) {
2085         if (string == null) {
2086             addViolation(
2087                     problems,
2088                     Severity.WARNING,
2089                     ModelProblem.Version.V20,
2090                     fieldName,
2091                     sourceHint,
2092                     " is missing.",
2093                     tracker);
2094             return false;
2095         }
2096 
2097         Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0);
2098 
2099         if (!validateVersion(EMPTY, fieldName, problems, errOn30, Version.V20, string, sourceHint, tracker)) {
2100             return false;
2101         }
2102 
2103         if (string.isEmpty() || "RELEASE".equals(string) || "LATEST".equals(string)) {
2104             addViolation(
2105                     problems,
2106                     errOn30,
2107                     Version.V20,
2108                     fieldName,
2109                     sourceHint,
2110                     "must be a valid version but is '" + string + "'.",
2111                     tracker);
2112             return false;
2113         }
2114 
2115         return true;
2116     }
2117 
2118     private static void addViolation(
2119             ModelProblemCollector problems,
2120             Severity severity,
2121             Version version,
2122             String fieldName,
2123             String sourceHint,
2124             String message,
2125             InputLocationTracker tracker) {
2126         StringBuilder buffer = new StringBuilder(256);
2127         buffer.append('\'').append(fieldName).append('\'');
2128 
2129         if (sourceHint != null) {
2130             buffer.append(" for ").append(sourceHint);
2131         }
2132 
2133         buffer.append(' ').append(message);
2134 
2135         problems.add(severity, version, buffer.toString(), getLocation(fieldName, tracker));
2136     }
2137 
2138     private static InputLocation getLocation(String fieldName, InputLocationTracker tracker) {
2139         InputLocation location = null;
2140 
2141         if (tracker != null) {
2142             if (fieldName != null) {
2143                 Object key = fieldName;
2144 
2145                 int idx = fieldName.lastIndexOf('.');
2146                 if (idx >= 0) {
2147                     fieldName = fieldName.substring(idx + 1);
2148                     key = fieldName;
2149                 }
2150 
2151                 if (fieldName.endsWith("]")) {
2152                     key = fieldName.substring(fieldName.lastIndexOf('[') + 1, fieldName.length() - 1);
2153                     try {
2154                         key = Integer.valueOf(key.toString());
2155                     } catch (NumberFormatException e) {
2156                         // use key as is
2157                     }
2158                 }
2159 
2160                 location = tracker.getLocation(key);
2161             }
2162 
2163             if (location == null) {
2164                 location = tracker.getLocation(EMPTY);
2165             }
2166         }
2167 
2168         return location;
2169     }
2170 
2171     private static boolean equals(String s1, String s2) {
2172         String c1 = s1 == null ? "" : s1.trim();
2173         String c2 = s2 == null ? "" : s2.trim();
2174         return c1.equals(c2);
2175     }
2176 
2177     private static Severity getSeverity(int validationLevel, int errorThreshold) {
2178         if (validationLevel < errorThreshold) {
2179             return Severity.WARNING;
2180         } else {
2181             return Severity.ERROR;
2182         }
2183     }
2184 }