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