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