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