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