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