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