1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.buildcache.checksum;
20
21 import javax.annotation.Nonnull;
22
23 import java.io.ByteArrayOutputStream;
24 import java.io.File;
25 import java.io.IOException;
26 import java.io.Writer;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.DirectoryStream;
29 import java.nio.file.FileVisitResult;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.nio.file.SimpleFileVisitor;
34 import java.nio.file.attribute.BasicFileAttributes;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Optional;
44 import java.util.Properties;
45 import java.util.Set;
46 import java.util.SortedMap;
47 import java.util.SortedSet;
48 import java.util.TreeMap;
49 import java.util.TreeSet;
50 import java.util.concurrent.atomic.AtomicInteger;
51 import java.util.function.Predicate;
52
53 import org.apache.commons.lang3.Strings;
54 import org.apache.maven.artifact.Artifact;
55 import org.apache.maven.artifact.DefaultArtifact;
56 import org.apache.maven.artifact.handler.ArtifactHandler;
57 import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
58 import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
59 import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
60 import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
61 import org.apache.maven.artifact.versioning.VersionRange;
62 import org.apache.maven.buildcache.CacheUtils;
63 import org.apache.maven.buildcache.MultiModuleSupport;
64 import org.apache.maven.buildcache.NormalizedModelProvider;
65 import org.apache.maven.buildcache.PluginScanConfig;
66 import org.apache.maven.buildcache.ProjectInputCalculator;
67 import org.apache.maven.buildcache.RemoteCacheRepository;
68 import org.apache.maven.buildcache.ScanConfigProperties;
69 import org.apache.maven.buildcache.Xpp3DomUtils;
70 import org.apache.maven.buildcache.checksum.exclude.ExclusionResolver;
71 import org.apache.maven.buildcache.hash.HashAlgorithm;
72 import org.apache.maven.buildcache.hash.HashChecksum;
73 import org.apache.maven.buildcache.xml.CacheConfig;
74 import org.apache.maven.buildcache.xml.DtoUtils;
75 import org.apache.maven.buildcache.xml.build.DigestItem;
76 import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
77 import org.apache.maven.buildcache.xml.config.Include;
78 import org.apache.maven.execution.MavenSession;
79 import org.apache.maven.model.Dependency;
80 import org.apache.maven.model.Exclusion;
81 import org.apache.maven.model.Model;
82 import org.apache.maven.model.Plugin;
83 import org.apache.maven.model.PluginExecution;
84 import org.apache.maven.model.Resource;
85 import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
86 import org.apache.maven.project.MavenProject;
87 import org.codehaus.plexus.util.WriterFactory;
88 import org.eclipse.aether.RepositorySystem;
89 import org.eclipse.aether.artifact.DefaultArtifactType;
90 import org.eclipse.aether.resolution.ArtifactRequest;
91 import org.eclipse.aether.resolution.ArtifactResolutionException;
92 import org.eclipse.aether.resolution.ArtifactResult;
93 import org.slf4j.Logger;
94 import org.slf4j.LoggerFactory;
95
96 import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
97 import static org.apache.commons.lang3.StringUtils.isBlank;
98 import static org.apache.commons.lang3.StringUtils.replaceEachRepeatedly;
99 import static org.apache.commons.lang3.StringUtils.stripToEmpty;
100 import static org.apache.maven.buildcache.CacheUtils.isPom;
101 import static org.apache.maven.buildcache.CacheUtils.isSnapshot;
102 import static org.apache.maven.buildcache.xml.CacheConfigImpl.CACHE_ENABLED_PROPERTY_NAME;
103 import static org.apache.maven.buildcache.xml.CacheConfigImpl.CACHE_SKIP;
104 import static org.apache.maven.buildcache.xml.CacheConfigImpl.RESTORE_GENERATED_SOURCES_PROPERTY_NAME;
105 import static org.apache.maven.buildcache.xml.CacheConfigImpl.RESTORE_ON_DISK_ARTIFACTS_PROPERTY_NAME;
106
107
108
109
110 public class MavenProjectInput {
111
112
113
114
115 public static final String CACHE_IMPLEMENTATION_VERSION = "v1.1";
116
117
118
119
120 private static final String CACHE_INPUT_GLOB_NAME = "maven.build.cache.input.glob";
121
122
123
124
125 private static final String CACHE_INPUT_NAME = "maven.build.cache.input";
126
127
128
129 private static final String CACHE_PROCESS_PLUGINS = "maven.build.cache.processPlugins";
130
131 private static final Logger LOGGER = LoggerFactory.getLogger(MavenProjectInput.class);
132
133 private final MavenProject project;
134 private final MavenSession session;
135 private final RemoteCacheRepository remoteCache;
136 private final RepositorySystem repoSystem;
137 private final CacheConfig config;
138 private final PathIgnoringCaseComparator fileComparator;
139 private final NormalizedModelProvider normalizedModelProvider;
140 private final MultiModuleSupport multiModuleSupport;
141 private final ProjectInputCalculator projectInputCalculator;
142 private final Path baseDirPath;
143 private final ArtifactHandlerManager artifactHandlerManager;
144
145
146
147
148 private final String projectGlob;
149
150 private final ExclusionResolver exclusionResolver;
151
152 private final boolean processPlugins;
153 private final String tmpDir;
154
155 @SuppressWarnings("checkstyle:parameternumber")
156 public MavenProjectInput(
157 MavenProject project,
158 NormalizedModelProvider normalizedModelProvider,
159 MultiModuleSupport multiModuleSupport,
160 ProjectInputCalculator projectInputCalculator,
161 MavenSession session,
162 CacheConfig config,
163 RepositorySystem repoSystem,
164 RemoteCacheRepository remoteCache,
165 ArtifactHandlerManager artifactHandlerManager) {
166 this.project = project;
167 this.normalizedModelProvider = normalizedModelProvider;
168 this.multiModuleSupport = multiModuleSupport;
169 this.projectInputCalculator = projectInputCalculator;
170 this.session = session;
171 this.config = config;
172 this.baseDirPath = project.getBasedir().toPath().toAbsolutePath();
173 this.repoSystem = repoSystem;
174 this.remoteCache = remoteCache;
175 Properties properties = project.getProperties();
176 this.projectGlob = properties.getProperty(CACHE_INPUT_GLOB_NAME, config.getDefaultGlob());
177 this.processPlugins =
178 Boolean.parseBoolean(properties.getProperty(CACHE_PROCESS_PLUGINS, config.isProcessPlugins()));
179 this.tmpDir = System.getProperty("java.io.tmpdir");
180
181 this.exclusionResolver = new ExclusionResolver(project, config);
182
183 this.fileComparator = new PathIgnoringCaseComparator();
184 this.artifactHandlerManager = artifactHandlerManager;
185 }
186
187 public ProjectsInputInfo calculateChecksum() throws IOException {
188 final long t0 = System.currentTimeMillis();
189
190 final String effectivePom = getEffectivePom(normalizedModelProvider.normalizedModel(project));
191 final SortedSet<Path> inputFiles = isPom(project) ? Collections.emptySortedSet() : getInputFiles();
192 final SortedMap<String, String> dependenciesChecksum = getMutableDependencies();
193 final SortedMap<String, String> pluginDependenciesChecksum = getMutablePluginDependencies();
194
195 final long t1 = System.currentTimeMillis();
196
197
198 final int count = 1
199 + (config.calculateProjectVersionChecksum() ? 1 : 0)
200 + 2 * inputFiles.size()
201 + dependenciesChecksum.size()
202 + pluginDependenciesChecksum.size();
203
204 final List<DigestItem> items = new ArrayList<>(count);
205 final HashChecksum checksum = config.getHashFactory().createChecksum(count);
206
207 Optional<ProjectsInputInfo> baselineHolder = Optional.empty();
208 if (config.isBaselineDiffEnabled()) {
209 baselineHolder =
210 remoteCache.findBaselineBuild(project).map(b -> b.getDto().getProjectsInputInfo());
211 }
212
213 if (config.calculateProjectVersionChecksum()) {
214 DigestItem projectVersion = new DigestItem();
215 projectVersion.setType("version");
216 projectVersion.setIsText("yes");
217 projectVersion.setValue(project.getVersion());
218 items.add(projectVersion);
219
220 checksum.update(project.getVersion().getBytes(StandardCharsets.UTF_8));
221 }
222
223 DigestItem effectivePomChecksum = DigestUtils.pom(checksum, effectivePom);
224 items.add(effectivePomChecksum);
225 final boolean compareWithBaseline = config.isBaselineDiffEnabled() && baselineHolder.isPresent();
226 if (compareWithBaseline) {
227 checkEffectivePomMatch(baselineHolder.get(), effectivePomChecksum);
228 }
229
230 boolean sourcesMatched = true;
231 for (Path file : inputFiles) {
232 DigestItem fileDigest = DigestUtils.file(checksum, baseDirPath, file);
233 items.add(fileDigest);
234 if (compareWithBaseline) {
235 sourcesMatched &= checkItemMatchesBaseline(baselineHolder.get(), fileDigest);
236 }
237 }
238 if (compareWithBaseline) {
239 LOGGER.info("Source code: {}", sourcesMatched ? "MATCHED" : "OUT OF DATE");
240 }
241
242 boolean dependenciesMatched = true;
243 for (Map.Entry<String, String> entry : dependenciesChecksum.entrySet()) {
244 DigestItem dependencyDigest = DigestUtils.dependency(checksum, entry.getKey(), entry.getValue());
245 items.add(dependencyDigest);
246 if (compareWithBaseline) {
247 dependenciesMatched &= checkItemMatchesBaseline(baselineHolder.get(), dependencyDigest);
248 }
249 }
250
251 if (compareWithBaseline) {
252 LOGGER.info("Dependencies: {}", dependenciesMatched ? "MATCHED" : "OUT OF DATE");
253 }
254
255 boolean pluginDependenciesMatched = true;
256 for (Map.Entry<String, String> entry : pluginDependenciesChecksum.entrySet()) {
257 DigestItem dependencyDigest = DigestUtils.pluginDependency(checksum, entry.getKey(), entry.getValue());
258 items.add(dependencyDigest);
259 if (compareWithBaseline) {
260 pluginDependenciesMatched &= checkItemMatchesBaseline(baselineHolder.get(), dependencyDigest);
261 }
262 }
263
264 if (compareWithBaseline) {
265 LOGGER.info("Plugin dependencies: {}", pluginDependenciesMatched ? "MATCHED" : "OUT OF DATE");
266 }
267
268 final ProjectsInputInfo projectsInputInfoType = new ProjectsInputInfo();
269 projectsInputInfoType.setChecksum(checksum.digest());
270 projectsInputInfoType.getItems().addAll(items);
271
272 final long t2 = System.currentTimeMillis();
273
274 if (LOGGER.isDebugEnabled()) {
275 for (DigestItem item : projectsInputInfoType.getItems()) {
276 LOGGER.debug("Hash calculated, item: {}, hash: {}", item.getType(), item.getHash());
277 }
278 }
279
280 LOGGER.info(
281 "Project inputs calculated in {} ms. {} checksum [{}] calculated in {} ms.",
282 t1 - t0,
283 config.getHashFactory().getAlgorithm(),
284 projectsInputInfoType.getChecksum(),
285 t2 - t1);
286 return projectsInputInfoType;
287 }
288
289 private void checkEffectivePomMatch(ProjectsInputInfo baselineBuild, DigestItem effectivePomChecksum) {
290 Optional<DigestItem> pomHolder = Optional.empty();
291 for (DigestItem it : baselineBuild.getItems()) {
292 if (it.getType().equals("pom")) {
293 pomHolder = Optional.of(it);
294 break;
295 }
296 }
297
298 if (pomHolder.isPresent()) {
299 DigestItem pomItem = pomHolder.get();
300 final boolean matches = Strings.CS.equals(pomItem.getHash(), effectivePomChecksum.getHash());
301 if (!matches) {
302 LOGGER.info(
303 "Mismatch in effective poms. Current: {}, remote: {}",
304 effectivePomChecksum.getHash(),
305 pomItem.getHash());
306 }
307 LOGGER.info("Effective pom: {}", matches ? "MATCHED" : "OUT OF DATE");
308 }
309 }
310
311 private boolean checkItemMatchesBaseline(ProjectsInputInfo baselineBuild, DigestItem fileDigest) {
312 Optional<DigestItem> baselineFileDigest = Optional.empty();
313 for (DigestItem it : baselineBuild.getItems()) {
314 if (it.getType().equals(fileDigest.getType())
315 && fileDigest.getValue().equals(it.getValue().trim())) {
316 baselineFileDigest = Optional.of(it);
317 break;
318 }
319 }
320
321 boolean matched = false;
322 if (baselineFileDigest.isPresent()) {
323 String hash = baselineFileDigest.get().getHash();
324 matched = Strings.CS.equals(hash, fileDigest.getHash());
325 if (!matched) {
326 LOGGER.info(
327 "Mismatch in {}: {}. Local hash: {}, remote: {}",
328 fileDigest.getType(),
329 fileDigest.getValue(),
330 fileDigest.getHash(),
331 hash);
332 }
333 } else {
334 LOGGER.info("Mismatch in {}: {}. Not found in remote cache", fileDigest.getType(), fileDigest.getValue());
335 }
336 return matched;
337 }
338
339
340
341
342 private String getEffectivePom(Model prototype) throws IOException {
343 ByteArrayOutputStream output = new ByteArrayOutputStream();
344
345 try (Writer writer = WriterFactory.newXmlWriter(output)) {
346 new MavenXpp3Writer().write(writer, prototype);
347
348
349 final String[] searchList = {baseDirPath.toString(), "\\", "windows", "linux"};
350 final String[] replacementList = {"", "/", "os.classifier", "os.classifier"};
351 return replaceEachRepeatedly(output.toString(), searchList, replacementList);
352 }
353 }
354
355 private SortedSet<Path> getInputFiles() {
356 long start = System.currentTimeMillis();
357 HashSet<WalkKey> visitedDirs = new HashSet<>();
358 ArrayList<Path> collectedFiles = new ArrayList<>();
359
360 org.apache.maven.model.Build build = project.getBuild();
361
362 final boolean recursive = true;
363 startWalk(Paths.get(build.getSourceDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
364 for (Resource resource : build.getResources()) {
365 startWalk(Paths.get(resource.getDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
366 }
367
368 startWalk(Paths.get(build.getTestSourceDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
369 for (Resource testResource : build.getTestResources()) {
370 startWalk(Paths.get(testResource.getDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
371 }
372
373 Properties properties = project.getProperties();
374 for (String name : properties.stringPropertyNames()) {
375 if (name.startsWith(CACHE_INPUT_NAME) && !CACHE_INPUT_GLOB_NAME.equals(name)) {
376 String path = properties.getProperty(name);
377 startWalk(Paths.get(path), projectGlob, recursive, collectedFiles, visitedDirs);
378 }
379 }
380
381 List<Include> includes = config.getGlobalIncludePaths();
382 for (Include include : includes) {
383 final String path = include.getValue();
384 final String glob = defaultIfEmpty(include.getGlob(), projectGlob);
385 startWalk(Paths.get(path), glob, include.isRecursive(), collectedFiles, visitedDirs);
386 }
387
388 long walkKnownPathsFinished = System.currentTimeMillis() - start;
389
390 LOGGER.info(
391 "Scanning plugins configurations to find input files. Probing is {}",
392 processPlugins
393 ? "enabled, values will be checked for presence in file system"
394 : "disabled, only tags with attribute " + CACHE_INPUT_NAME + "=\"true\" will be added");
395
396 if (processPlugins) {
397 collectFromPlugins(collectedFiles, visitedDirs);
398 } else {
399 LOGGER.info("Skipping check plugins scan (probing is disabled by config)");
400 }
401
402 long pluginsFinished = System.currentTimeMillis() - start - walkKnownPathsFinished;
403
404 TreeSet<Path> sorted = new TreeSet<>(fileComparator);
405 for (Path collectedFile : collectedFiles) {
406 sorted.add(collectedFile.normalize().toAbsolutePath());
407 }
408
409 LOGGER.info(
410 "Found {} input files. Project dir processing: {}, plugins: {} millis",
411 sorted.size(),
412 walkKnownPathsFinished,
413 pluginsFinished);
414 LOGGER.debug("Src input: {}", sorted);
415
416 return sorted;
417 }
418
419 private Path convertToAbsolutePath(Path path) {
420 Path resolvedPath = path.isAbsolute() ? path : baseDirPath.resolve(path);
421 return resolvedPath.toAbsolutePath().normalize();
422 }
423
424
425
426
427 private void startWalk(
428 Path candidate, String glob, boolean recursive, List<Path> collectedFiles, Set<WalkKey> visitedDirs) {
429 Path normalized = convertToAbsolutePath(candidate);
430 WalkKey key = new WalkKey(normalized, glob, recursive);
431 if (visitedDirs.contains(key) || !Files.exists(normalized)) {
432 return;
433 }
434
435 if (Files.isDirectory(normalized)) {
436 if (baseDirPath.startsWith(normalized)) {
437 key = new WalkKey(normalized, glob, false);
438 }
439 try {
440 walkDir(key, collectedFiles, visitedDirs);
441 visitedDirs.add(key);
442 } catch (IOException e) {
443 throw new RuntimeException(e);
444 }
445 } else {
446 if (!exclusionResolver.excludesPath(normalized)) {
447 LOGGER.debug("Adding: {}", normalized);
448 collectedFiles.add(normalized);
449 }
450 }
451 }
452
453 private void collectFromPlugins(List<Path> files, HashSet<WalkKey> visitedDirs) {
454 List<Plugin> plugins = project.getBuild().getPlugins();
455 for (Plugin plugin : plugins) {
456 PluginScanConfig scanConfig = config.getPluginDirScanConfig(plugin);
457
458 if (scanConfig.isSkip()) {
459 LOGGER.debug("Skipping plugin config scan (skip by config): {}", plugin.getArtifactId());
460 continue;
461 }
462
463 Object configuration = plugin.getConfiguration();
464 LOGGER.debug("Processing plugin config: {}", plugin.getArtifactId());
465 if (configuration != null) {
466 addInputsFromPluginConfigs(Xpp3DomUtils.getChildren(configuration), scanConfig, files, visitedDirs);
467 }
468
469 for (PluginExecution exec : plugin.getExecutions()) {
470 final PluginScanConfig executionScanConfig = config.getExecutionDirScanConfig(plugin, exec);
471 PluginScanConfig mergedConfig = scanConfig.mergeWith(executionScanConfig);
472
473 if (mergedConfig.isSkip()) {
474 LOGGER.debug(
475 "Skipping plugin execution config scan (skip by config): {}, execId: {}",
476 plugin.getArtifactId(),
477 exec.getId());
478 continue;
479 }
480
481 Object execConfiguration = exec.getConfiguration();
482 LOGGER.debug("Processing plugin: {}, execution: {}", plugin.getArtifactId(), exec.getId());
483
484 if (execConfiguration != null) {
485 addInputsFromPluginConfigs(
486 Xpp3DomUtils.getChildren(execConfiguration), mergedConfig, files, visitedDirs);
487 }
488 }
489 }
490 }
491
492 private Path walkDir(final WalkKey key, final List<Path> collectedFiles, final Set<WalkKey> visitedDirs)
493 throws IOException {
494 return Files.walkFileTree(key.getPath(), new SimpleFileVisitor<Path>() {
495
496 @Override
497 public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes)
498 throws IOException {
499 WalkKey currentDirKey =
500 new WalkKey(path.toAbsolutePath().normalize(), key.getGlob(), key.isRecursive());
501 if (isHidden(path)) {
502 LOGGER.debug("Skipping subtree (hidden): {}", path);
503 return FileVisitResult.SKIP_SUBTREE;
504 } else if (!isReadable(path)) {
505 LOGGER.debug("Skipping subtree (not readable): {}", path);
506 return FileVisitResult.SKIP_SUBTREE;
507 } else if (exclusionResolver.excludesPath(path)) {
508 LOGGER.debug("Skipping subtree (blacklisted): {}", path);
509 return FileVisitResult.SKIP_SUBTREE;
510 } else if (visitedDirs.contains(currentDirKey)) {
511 LOGGER.debug("Skipping subtree (visited): {}", path);
512 return FileVisitResult.SKIP_SUBTREE;
513 }
514
515 walkDirectoryFiles(path, collectedFiles, key.getGlob(), exclusionResolver::excludesPath);
516
517 if (!key.isRecursive()) {
518 LOGGER.debug("Skipping subtree (non recursive): {}", path);
519 return FileVisitResult.SKIP_SUBTREE;
520 }
521
522 LOGGER.debug("Visiting subtree: {}", path);
523 return FileVisitResult.CONTINUE;
524 }
525
526 @Override
527 public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException {
528 LOGGER.debug("Skipping subtree (exception: {}): {}", exc, path);
529 return FileVisitResult.SKIP_SUBTREE;
530 }
531 });
532 }
533
534 private void addInputsFromPluginConfigs(
535 Object[] configurationChildren,
536 PluginScanConfig scanConfig,
537 List<Path> files,
538 HashSet<WalkKey> visitedDirs) {
539 if (configurationChildren == null) {
540 return;
541 }
542
543 for (Object configChild : configurationChildren) {
544 String tagName = Xpp3DomUtils.getName(configChild);
545 String tagValue = Xpp3DomUtils.getValue(configChild);
546
547 if (!scanConfig.accept(tagName)) {
548 LOGGER.debug("Skipping property (scan config)): {}, value: {}", tagName, stripToEmpty(tagValue));
549 continue;
550 }
551
552 LOGGER.debug("Checking xml tag. Tag: {}, value: {}", tagName, stripToEmpty(tagValue));
553
554 addInputsFromPluginConfigs(Xpp3DomUtils.getChildren(configChild), scanConfig, files, visitedDirs);
555
556 final ScanConfigProperties propertyConfig = scanConfig.getTagScanProperties(tagName);
557 final String glob = defaultIfEmpty(propertyConfig.getGlob(), projectGlob);
558 if ("true".equals(Xpp3DomUtils.getAttribute(configChild, CACHE_INPUT_NAME))) {
559 LOGGER.info(
560 "Found tag marked with {} attribute. Tag: {}, value: {}", CACHE_INPUT_NAME, tagName, tagValue);
561 startWalk(Paths.get(tagValue), glob, propertyConfig.isRecursive(), files, visitedDirs);
562 } else {
563 final Path candidate = getPathOrNull(tagValue);
564 if (candidate != null) {
565 startWalk(candidate, glob, propertyConfig.isRecursive(), files, visitedDirs);
566 if ("descriptorRef"
567 .equals(tagName)) {
568
569 startWalk(Paths.get(tagValue + ".xml"), glob, propertyConfig.isRecursive(), files, visitedDirs);
570 }
571 }
572 }
573 }
574 }
575
576 private Path getPathOrNull(String text) {
577
578 if (isBlank(text)) {
579
580 } else if (Strings.CI.equalsAny(text, "true", "false", "utf-8", "null", "\\")
581 || Strings.CS.contains(text, "*")
582 || (Strings.CS.contains(text, ":") && !Strings.CS.contains(text, ":\\"))
583 || Strings.CS.startsWithAny(text, "com.", "org.", "io.", "java.", "javax.")
584 || Strings.CS.startsWithAny(text, "${env.")
585 || Strings.CS.startsWithAny(
586 text,
587 "http:",
588 "https:",
589 "scm:",
590 "ssh:",
591 "git:",
592 "svn:",
593 "cp:",
594 "classpath:"))
595 {
596 LOGGER.debug("Skipping directory (blacklisted literal): {}", text);
597 } else if (Strings.CS.startsWithAny(text, tmpDir))
598 {
599 LOGGER.debug("Skipping directory (temp dir): {}", text);
600 } else {
601 try {
602 return Paths.get(text);
603 } catch (Exception ignore) {
604 LOGGER.debug("Skipping directory (invalid path): {}", text);
605 }
606 }
607 return null;
608 }
609
610 static void walkDirectoryFiles(Path dir, List<Path> collectedFiles, String glob, Predicate<Path> mustBeSkipped) {
611 if (!Files.isDirectory(dir)) {
612 return;
613 }
614
615 try {
616 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, glob)) {
617 for (Path entry : stream) {
618 if (mustBeSkipped.test(entry)) {
619 continue;
620 }
621 File file = entry.toFile();
622 if (file.isFile() && !isHidden(entry) && isReadable(entry)) {
623 collectedFiles.add(entry);
624 }
625 }
626 }
627 } catch (IOException e) {
628 throw new RuntimeException("Cannot process directory: " + dir, e);
629 }
630 }
631
632 private static boolean isHidden(Path entry) throws IOException {
633 return Files.isHidden(entry) || entry.toFile().getName().startsWith(".");
634 }
635
636 private static boolean isReadable(Path entry) throws IOException {
637 return Files.isReadable(entry);
638 }
639
640 private SortedMap<String, String> getMutableDependencies() throws IOException {
641 return getMutableDependenciesHashes("", project.getDependencies());
642 }
643
644 private SortedMap<String, String> getMutablePluginDependencies() throws IOException {
645 Map<String, AtomicInteger> keyPrefixOccurrenceIndex = new HashMap<>();
646 SortedMap<String, String> fullMap = new TreeMap<>();
647 for (Plugin plugin : project.getBuildPlugins()) {
648 if (config.isPluginDependenciesExcluded(plugin)) {
649 continue;
650 }
651
652 String rawKeyPrefix = KeyUtils.getVersionlessArtifactKey(createPluginArtifact(plugin));
653 int occurrenceIndex = keyPrefixOccurrenceIndex
654 .computeIfAbsent(rawKeyPrefix, k -> new AtomicInteger())
655 .getAndIncrement();
656 fullMap.putAll(
657 getMutableDependenciesHashes(rawKeyPrefix + "|" + occurrenceIndex + "|", plugin.getDependencies()));
658 }
659 return fullMap;
660 }
661
662 public Artifact createPluginArtifact(Plugin plugin) {
663
664 VersionRange versionRange;
665 try {
666 versionRange = VersionRange.createFromVersionSpec(plugin.getVersion());
667 } catch (InvalidVersionSpecificationException e) {
668 LOGGER.error(
669 String.format(
670 "Invalid version specification '%s' creating plugin artifact '%s'.",
671 plugin.getVersion(), plugin),
672 e);
673
674 throw new RuntimeException(e);
675 }
676
677 return createArtifact(
678 plugin.getGroupId(),
679 plugin.getArtifactId(),
680 versionRange,
681 "maven-plugin",
682 null,
683 Artifact.SCOPE_RUNTIME,
684 null,
685 false);
686 }
687
688 private Artifact createArtifact(
689 String groupId,
690 String artifactId,
691 VersionRange versionRange,
692 String type,
693 String classifier,
694 String scope,
695 String inheritedScope,
696 boolean optional) {
697
698 String desiredScope = Artifact.SCOPE_RUNTIME;
699
700 if (inheritedScope == null) {
701 desiredScope = scope;
702 } else if (Artifact.SCOPE_TEST.equals(scope) || Artifact.SCOPE_PROVIDED.equals(scope)) {
703 return null;
704 } else if (Artifact.SCOPE_COMPILE.equals(scope) && Artifact.SCOPE_COMPILE.equals(inheritedScope)) {
705
706 desiredScope = Artifact.SCOPE_COMPILE;
707 }
708
709 if (Artifact.SCOPE_TEST.equals(inheritedScope)) {
710 desiredScope = Artifact.SCOPE_TEST;
711 }
712
713 if (Artifact.SCOPE_PROVIDED.equals(inheritedScope)) {
714 desiredScope = Artifact.SCOPE_PROVIDED;
715 }
716
717 if (Artifact.SCOPE_SYSTEM.equals(scope)) {
718
719 desiredScope = Artifact.SCOPE_SYSTEM;
720 }
721 ArtifactHandler handler = artifactHandlerManager.getArtifactHandler(type);
722
723 return new DefaultArtifact(
724 groupId, artifactId, versionRange, desiredScope, type, classifier, handler, optional);
725 }
726
727 public Artifact createDependencyArtifact(Dependency d) {
728 VersionRange versionRange;
729 try {
730 versionRange = VersionRange.createFromVersionSpec(d.getVersion());
731 } catch (InvalidVersionSpecificationException e) {
732 LOGGER.error(
733 String.format(
734 "Invalid version specification '%s' creating dependency artifact '%s'.", d.getVersion(), d),
735 e);
736
737 throw new RuntimeException(e);
738 }
739
740 Artifact artifact = createArtifact(
741 d.getGroupId(),
742 d.getArtifactId(),
743 versionRange,
744 d.getType(),
745 d.getClassifier(),
746 d.getScope(),
747 null,
748 d.isOptional());
749
750 if (Artifact.SCOPE_SYSTEM.equals(d.getScope()) && d.getSystemPath() != null) {
751 artifact.setFile(new File(d.getSystemPath()));
752 }
753
754 if (!d.getExclusions().isEmpty()) {
755 List<String> exclusions = new ArrayList<>();
756
757 for (Exclusion exclusion : d.getExclusions()) {
758 exclusions.add(exclusion.getGroupId() + ':' + exclusion.getArtifactId());
759 }
760
761 artifact.setDependencyFilter(new ExcludesArtifactFilter(exclusions));
762 }
763
764 return artifact;
765 }
766
767 private SortedMap<String, String> getMutableDependenciesHashes(String keyPrefix, List<Dependency> dependencies)
768 throws IOException {
769 SortedMap<String, String> result = new TreeMap<>();
770
771 for (Dependency dependency : dependencies) {
772
773 if (CacheUtils.isPom(dependency)) {
774
775
776
777
778 continue;
779 }
780
781 final String versionSpec = dependency.getVersion();
782
783
784 MavenProject dependencyProject = versionSpec == null
785 ? null
786 : multiModuleSupport
787 .tryToResolveProject(dependency.getGroupId(), dependency.getArtifactId(), versionSpec)
788 .orElse(null);
789
790
791
792
793 if (dependencyProject == null && isDynamicVersion(versionSpec)) {
794 dependencyProject = tryResolveReactorProjectByGA(dependency).orElse(null);
795 }
796
797 boolean isSnapshot = isSnapshot(versionSpec);
798 if (dependencyProject == null && !isSnapshot) {
799
800 continue;
801 }
802 String projectHash;
803 if (dependencyProject != null)
804 {
805 projectHash =
806 projectInputCalculator.calculateInput(dependencyProject).getChecksum();
807 } else
808 {
809 try {
810 DigestItem resolved = resolveArtifact(dependency);
811 projectHash = resolved.getHash();
812 } catch (ArtifactResolutionException | InvalidVersionSpecificationException e) {
813 throw new IOException(e);
814 }
815 }
816 result.put(
817 keyPrefix + KeyUtils.getVersionlessArtifactKey(createDependencyArtifact(dependency)), projectHash);
818 }
819 return result;
820 }
821
822 @Nonnull
823 private DigestItem resolveArtifact(final Dependency dependency)
824 throws IOException, ArtifactResolutionException, InvalidVersionSpecificationException {
825
826
827 if (Artifact.SCOPE_SYSTEM.equals(dependency.getScope()) && dependency.getSystemPath() != null) {
828 final Path systemPath = Paths.get(dependency.getSystemPath()).normalize();
829 if (!Files.exists(systemPath)) {
830 throw new DependencyNotResolvedException(
831 "System dependency file does not exist: " + systemPath + " for dependency: " + dependency);
832 }
833 final HashAlgorithm algorithm = config.getHashFactory().createAlgorithm();
834 final String hash = algorithm.hash(systemPath);
835 final Artifact artifact = createDependencyArtifact(dependency);
836 return DtoUtils.createDigestedFile(artifact, hash);
837 }
838
839 org.eclipse.aether.artifact.Artifact dependencyArtifact = new org.eclipse.aether.artifact.DefaultArtifact(
840 dependency.getGroupId(),
841 dependency.getArtifactId(),
842 dependency.getClassifier(),
843 null,
844 dependency.getVersion(),
845 new DefaultArtifactType(dependency.getType()));
846 ArtifactRequest artifactRequest = new ArtifactRequest().setArtifact(dependencyArtifact);
847 artifactRequest.setRepositories(project.getRemoteProjectRepositories());
848
849 ArtifactResult result = repoSystem.resolveArtifact(session.getRepositorySession(), artifactRequest);
850
851 if (!result.isResolved()) {
852 throw new DependencyNotResolvedException("Cannot resolve in-project dependency: " + dependencyArtifact);
853 }
854
855 if (result.isMissing()) {
856 throw new DependencyNotResolvedException("Cannot resolve missing artifact: " + dependencyArtifact);
857 }
858
859 org.eclipse.aether.artifact.Artifact resolved = result.getArtifact();
860
861 Artifact artifact = createArtifact(
862 resolved.getGroupId(),
863 resolved.getArtifactId(),
864 VersionRange.createFromVersionSpec(resolved.getVersion()),
865 dependency.getType(),
866 resolved.getClassifier(),
867 dependency.getType(),
868 dependency.getScope(),
869 false);
870
871 final HashAlgorithm algorithm = config.getHashFactory().createAlgorithm();
872 final String hash = algorithm.hash(resolved.getFile().toPath());
873 return DtoUtils.createDigestedFile(artifact, hash);
874 }
875
876 private static boolean isDynamicVersion(String versionSpec) {
877 if (versionSpec == null) {
878 return true;
879 }
880 if ("LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
881 return true;
882 }
883
884 return versionSpec.startsWith("[") || versionSpec.startsWith("(") || versionSpec.contains(",");
885 }
886
887 private Optional<MavenProject> tryResolveReactorProjectByGA(Dependency dependency) {
888 final List<MavenProject> projects = session.getAllProjects();
889 if (projects == null || projects.isEmpty()) {
890 return Optional.empty();
891 }
892
893 final String groupId = dependency.getGroupId();
894 final String artifactId = dependency.getArtifactId();
895 final String versionSpec = dependency.getVersion();
896
897 for (MavenProject candidate : projects) {
898 if (!Objects.equals(groupId, candidate.getGroupId())
899 || !Objects.equals(artifactId, candidate.getArtifactId())) {
900 continue;
901 }
902
903
904 if (versionSpec == null || "LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
905 return Optional.of(candidate);
906 }
907
908
909 if (versionSpec.startsWith("[") || versionSpec.startsWith("(") || versionSpec.contains(",")) {
910 try {
911 VersionRange range = VersionRange.createFromVersionSpec(versionSpec);
912 if (range.containsVersion(new DefaultArtifactVersion(candidate.getVersion()))) {
913 return Optional.of(candidate);
914 }
915 } catch (InvalidVersionSpecificationException e) {
916
917 return Optional.empty();
918 }
919 }
920 }
921 return Optional.empty();
922 }
923
924
925
926
927 public static class PathIgnoringCaseComparator implements Comparator<Path> {
928
929 @Override
930 public int compare(Path f1, Path f2) {
931 String s1 = f1.toAbsolutePath().toString();
932 String s2 = f2.toAbsolutePath().toString();
933 if (File.separator.equals("\\")) {
934 s1 = s1.replaceAll("\\\\", "/");
935 s2 = s2.replaceAll("\\\\", "/");
936 }
937 return s1.compareToIgnoreCase(s2);
938 }
939 }
940
941
942
943
944
945
946
947 public static boolean isSkipCache(MavenProject project) {
948 return Boolean.parseBoolean(project.getProperties().getProperty(CACHE_SKIP, "false"));
949 }
950
951
952
953
954
955
956
957
958 public static boolean isRestoreGeneratedSources(MavenProject project) {
959 return Boolean.parseBoolean(
960 project.getProperties().getProperty(RESTORE_GENERATED_SOURCES_PROPERTY_NAME, "true"));
961 }
962
963
964
965
966
967
968
969
970 public static boolean isRestoreOnDiskArtifacts(MavenProject project) {
971 return Boolean.parseBoolean(
972 project.getProperties().getProperty(RESTORE_ON_DISK_ARTIFACTS_PROPERTY_NAME, "true"));
973 }
974
975
976
977
978
979
980
981
982 public static boolean isCacheDisabled(MavenProject project) {
983 return !Boolean.parseBoolean(project.getProperties().getProperty(CACHE_ENABLED_PROPERTY_NAME, "true"));
984 }
985 }