View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.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  * MavenProjectInput
109  */
110 public class MavenProjectInput {
111 
112     /**
113      * Version of cache implementation. It is recommended to change to simplify remote cache maintenance
114      */
115     public static final String CACHE_IMPLEMENTATION_VERSION = "v1.1";
116 
117     /**
118      * property name to pass glob value. The glob to be used to list directory files in plugins scanning
119      */
120     private static final String CACHE_INPUT_GLOB_NAME = "maven.build.cache.input.glob";
121     /**
122      * property name prefix to pass input files with project properties. smth like maven.build.cache.input.1 will be
123      * accepted
124      */
125     private static final String CACHE_INPUT_NAME = "maven.build.cache.input";
126     /**
127      * Flag to control if we should check values from plugin configs as file system objects
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      * The project glob to use every time there is no override
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         // hash items: effective pom + version + input files paths + input files contents + dependencies
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      * @param prototype effective model fully resolved by maven build. Do not pass here just parsed Model.
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             // normalize env specifics
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      * entry point for directory walk
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)) { // requested to walk parent, can do only non recursive
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)) { // hardcoded logic for assembly plugin which could reference files
568                         // omitting .xml suffix
569                         startWalk(Paths.get(tagValue + ".xml"), glob, propertyConfig.isRecursive(), files, visitedDirs);
570                     }
571                 }
572             }
573         }
574     }
575 
576     private Path getPathOrNull(String text) {
577         // small optimization to not probe not-paths
578         if (isBlank(text)) {
579             // do not even bother logging about blank/null values
580         } else if (Strings.CI.equalsAny(text, "true", "false", "utf-8", "null", "\\") // common values
581                 || Strings.CS.contains(text, "*") // tag value is a glob or regex - unclear how to process
582                 || (Strings.CS.contains(text, ":") && !Strings.CS.contains(text, ":\\")) // artifactId
583                 || Strings.CS.startsWithAny(text, "com.", "org.", "io.", "java.", "javax.") // java packages
584                 || Strings.CS.startsWithAny(text, "${env.") // env variables in maven notation
585                 || Strings.CS.startsWithAny(
586                         text,
587                         "http:",
588                         "https:",
589                         "scm:",
590                         "ssh:",
591                         "git:",
592                         "svn:",
593                         "cp:",
594                         "classpath:")) // urls identified by common protocols
595         {
596             LOGGER.debug("Skipping directory (blacklisted literal): {}", text);
597         } else if (Strings.CS.startsWithAny(text, tmpDir)) // tmp dir
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             // should not happen here
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     // CHECKSTYLE_OFF: ParameterNumber
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         // CHECKSTYLE_OFF: ParameterNumber
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             // added to retain compile artifactScope. Remove if you want compile inherited as runtime
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             // system scopes come through unchanged...
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             // should not happen here ?
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                 // POM dependency will be resolved by maven system to actual dependencies
775                 // and will contribute to effective pom.
776                 // Effective result will be recorded by #getNormalizedPom
777                 // so pom dependencies must be skipped as meaningless by themselves
778                 continue;
779             }
780 
781             final String versionSpec = dependency.getVersion();
782 
783             // saved to index by the end of dependency build
784             MavenProject dependencyProject = versionSpec == null
785                     ? null
786                     : multiModuleSupport
787                             .tryToResolveProject(dependency.getGroupId(), dependency.getArtifactId(), versionSpec)
788                             .orElse(null);
789 
790             // for dynamic versions (LATEST/RELEASE/ranges), reactor artifacts can be part of the build
791             // but cannot be resolved yet from the workspace (not built), so Aether may try remote download.
792             // If a matching reactor module exists, treat it as multi-module dependency and use project checksum.
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                 // external immutable dependency, should skip
800                 continue;
801             }
802             String projectHash;
803             if (dependencyProject != null) // part of multi module
804             {
805                 projectHash =
806                         projectInputCalculator.calculateInput(dependencyProject).getChecksum();
807             } else // this is a snapshot dependency
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         // system-scoped dependencies are local files (systemPath) and must NOT be resolved via Aether.
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         // Maven version ranges: [1.0,2.0), (1.0,), etc.
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             // For null/LATEST/RELEASE, accept the reactor module directly.
904             if (versionSpec == null || "LATEST".equals(versionSpec) || "RELEASE".equals(versionSpec)) {
905                 return Optional.of(candidate);
906             }
907 
908             // For ranges, only accept if reactor version fits the range.
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                     // If the spec is not parseable as range, don't guess.
917                     return Optional.empty();
918                 }
919             }
920         }
921         return Optional.empty();
922     }
923 
924     /**
925      * PathIgnoringCaseComparator
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      * Skip lookup on a per-project level via a property to force module rebuild
943      * e.g.{@code <maven.build.cache.skipCache>true<maven.build.cache.skipCache/>}
944      * @param project
945      * @return
946      */
947     public static boolean isSkipCache(MavenProject project) {
948         return Boolean.parseBoolean(project.getProperties().getProperty(CACHE_SKIP, "false"));
949     }
950 
951     /**
952      * Allow skipping generated sources restoration on a per-project level via a property (which defaults to true)
953      * e.g. {@code <maven.build.cache.restoreGeneratedSources>false<maven.build.cache.restoreGeneratedSources/>}.
954      *
955      * @param  project
956      * @return
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      * Allow skipping artifacts restoration on a per-project level via a property (which defaults to true)
965      * e.g. {@code <maven.build.cache.restoreOnDiskArtifacts>false<maven.build.cache.restoreOnDiskArtifacts/>}.
966      *
967      * @param  project
968      * @return
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      * Allow disabling caching entirely on a per-project level via a property - both artifact lookup and upload
977      * Defaults to false
978      * {@code <maven.build.cache.enabled>false<maven.build.cache.enabled/>}
979      * @param project
980      * @return
981      */
982     public static boolean isCacheDisabled(MavenProject project) {
983         return !Boolean.parseBoolean(project.getProperties().getProperty(CACHE_ENABLED_PROPERTY_NAME, "true"));
984     }
985 }