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