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