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