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