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