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.file.DirectoryStream;
28  import java.nio.file.FileVisitResult;
29  import java.nio.file.Files;
30  import java.nio.file.Path;
31  import java.nio.file.Paths;
32  import java.nio.file.SimpleFileVisitor;
33  import java.nio.file.attribute.BasicFileAttributes;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.Comparator;
37  import java.util.HashSet;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Optional;
41  import java.util.Properties;
42  import java.util.Set;
43  import java.util.SortedMap;
44  import java.util.SortedSet;
45  import java.util.TreeMap;
46  import java.util.TreeSet;
47  import java.util.function.Predicate;
48  
49  import org.apache.commons.lang3.StringUtils;
50  import org.apache.maven.artifact.Artifact;
51  import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
52  import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
53  import org.apache.maven.buildcache.CacheUtils;
54  import org.apache.maven.buildcache.MultiModuleSupport;
55  import org.apache.maven.buildcache.NormalizedModelProvider;
56  import org.apache.maven.buildcache.PluginScanConfig;
57  import org.apache.maven.buildcache.ProjectInputCalculator;
58  import org.apache.maven.buildcache.RemoteCacheRepository;
59  import org.apache.maven.buildcache.ScanConfigProperties;
60  import org.apache.maven.buildcache.Xpp3DomUtils;
61  import org.apache.maven.buildcache.checksum.exclude.ExclusionResolver;
62  import org.apache.maven.buildcache.hash.HashAlgorithm;
63  import org.apache.maven.buildcache.hash.HashChecksum;
64  import org.apache.maven.buildcache.xml.CacheConfig;
65  import org.apache.maven.buildcache.xml.DtoUtils;
66  import org.apache.maven.buildcache.xml.build.DigestItem;
67  import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
68  import org.apache.maven.buildcache.xml.config.Include;
69  import org.apache.maven.execution.MavenSession;
70  import org.apache.maven.model.Dependency;
71  import org.apache.maven.model.Model;
72  import org.apache.maven.model.Plugin;
73  import org.apache.maven.model.PluginExecution;
74  import org.apache.maven.model.Resource;
75  import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
76  import org.apache.maven.project.MavenProject;
77  import org.apache.maven.repository.RepositorySystem;
78  import org.codehaus.plexus.util.IOUtil;
79  import org.codehaus.plexus.util.WriterFactory;
80  import org.slf4j.Logger;
81  import org.slf4j.LoggerFactory;
82  
83  import static org.apache.commons.lang3.StringUtils.contains;
84  import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
85  import static org.apache.commons.lang3.StringUtils.equalsAnyIgnoreCase;
86  import static org.apache.commons.lang3.StringUtils.isBlank;
87  import static org.apache.commons.lang3.StringUtils.replaceEachRepeatedly;
88  import static org.apache.commons.lang3.StringUtils.startsWithAny;
89  import static org.apache.commons.lang3.StringUtils.stripToEmpty;
90  import static org.apache.maven.buildcache.CacheUtils.isPom;
91  import static org.apache.maven.buildcache.CacheUtils.isSnapshot;
92  import static org.apache.maven.buildcache.xml.CacheConfigImpl.CACHE_ENABLED_PROPERTY_NAME;
93  import static org.apache.maven.buildcache.xml.CacheConfigImpl.CACHE_SKIP;
94  import static org.apache.maven.buildcache.xml.CacheConfigImpl.RESTORE_GENERATED_SOURCES_PROPERTY_NAME;
95  
96  /**
97   * MavenProjectInput
98   */
99  public class MavenProjectInput {
100 
101     /**
102      * Version of hashing algorithm implementation. It is recommended to change to simplify remote cache maintenance
103      */
104     public static final String CACHE_IMPLEMENTATION_VERSION = "v1";
105 
106     /**
107      * property name to pass glob value. The glob to be used to list directory files in plugins scanning
108      */
109     private static final String CACHE_INPUT_GLOB_NAME = "maven.build.cache.input.glob";
110     /**
111      * property name prefix to pass input files with project properties. smth like maven.build.cache.input.1 will be
112      * accepted
113      */
114     private static final String CACHE_INPUT_NAME = "maven.build.cache.input";
115     /**
116      * Flag to control if we should check values from plugin configs as file system objects
117      */
118     private static final String CACHE_PROCESS_PLUGINS = "maven.build.cache.processPlugins";
119 
120     private static final Logger LOGGER = LoggerFactory.getLogger(MavenProjectInput.class);
121 
122     private final MavenProject project;
123     private final MavenSession session;
124     private final RemoteCacheRepository remoteCache;
125     private final RepositorySystem repoSystem;
126     private final CacheConfig config;
127     private final PathIgnoringCaseComparator fileComparator;
128     private final NormalizedModelProvider normalizedModelProvider;
129     private final MultiModuleSupport multiModuleSupport;
130     private final ProjectInputCalculator projectInputCalculator;
131     private final Path baseDirPath;
132     /**
133      * The project glob to use every time there is no override
134      */
135     private final String projectGlob;
136 
137     private final ExclusionResolver exclusionResolver;
138 
139     private final boolean processPlugins;
140     private final String tmpDir;
141 
142     @SuppressWarnings("checkstyle:parameternumber")
143     public MavenProjectInput(
144             MavenProject project,
145             NormalizedModelProvider normalizedModelProvider,
146             MultiModuleSupport multiModuleSupport,
147             ProjectInputCalculator projectInputCalculator,
148             MavenSession session,
149             CacheConfig config,
150             RepositorySystem repoSystem,
151             RemoteCacheRepository remoteCache) {
152         this.project = project;
153         this.normalizedModelProvider = normalizedModelProvider;
154         this.multiModuleSupport = multiModuleSupport;
155         this.projectInputCalculator = projectInputCalculator;
156         this.session = session;
157         this.config = config;
158         this.baseDirPath = project.getBasedir().toPath().toAbsolutePath();
159         this.repoSystem = repoSystem;
160         this.remoteCache = remoteCache;
161         Properties properties = project.getProperties();
162         this.projectGlob = properties.getProperty(CACHE_INPUT_GLOB_NAME, config.getDefaultGlob());
163         this.processPlugins =
164                 Boolean.parseBoolean(properties.getProperty(CACHE_PROCESS_PLUGINS, config.isProcessPlugins()));
165         this.tmpDir = System.getProperty("java.io.tmpdir");
166 
167         this.exclusionResolver = new ExclusionResolver(project, config);
168 
169         this.fileComparator = new PathIgnoringCaseComparator();
170     }
171 
172     public ProjectsInputInfo calculateChecksum() throws IOException {
173         final long t0 = System.currentTimeMillis();
174 
175         final String effectivePom = getEffectivePom(normalizedModelProvider.normalizedModel(project));
176         final SortedSet<Path> inputFiles = isPom(project) ? Collections.emptySortedSet() : getInputFiles();
177         final SortedMap<String, String> dependenciesChecksum = getMutableDependencies();
178 
179         final long t1 = System.currentTimeMillis();
180 
181         // hash items: effective pom + input files + dependencies
182         final int count = 1 + inputFiles.size() + dependenciesChecksum.size();
183         final List<DigestItem> items = new ArrayList<>(count);
184         final HashChecksum checksum = config.getHashFactory().createChecksum(count);
185 
186         Optional<ProjectsInputInfo> baselineHolder = Optional.empty();
187         if (config.isBaselineDiffEnabled()) {
188             baselineHolder =
189                     remoteCache.findBaselineBuild(project).map(b -> b.getDto().getProjectsInputInfo());
190         }
191 
192         DigestItem effectivePomChecksum = DigestUtils.pom(checksum, effectivePom);
193         items.add(effectivePomChecksum);
194         final boolean compareWithBaseline = config.isBaselineDiffEnabled() && baselineHolder.isPresent();
195         if (compareWithBaseline) {
196             checkEffectivePomMatch(baselineHolder.get(), effectivePomChecksum);
197         }
198 
199         boolean sourcesMatched = true;
200         for (Path file : inputFiles) {
201             DigestItem fileDigest = DigestUtils.file(checksum, baseDirPath, file);
202             items.add(fileDigest);
203             if (compareWithBaseline) {
204                 sourcesMatched &= checkItemMatchesBaseline(baselineHolder.get(), fileDigest);
205             }
206         }
207         if (compareWithBaseline) {
208             LOGGER.info("Source code: {}", sourcesMatched ? "MATCHED" : "OUT OF DATE");
209         }
210 
211         boolean dependenciesMatched = true;
212         for (Map.Entry<String, String> entry : dependenciesChecksum.entrySet()) {
213             DigestItem dependencyDigest = DigestUtils.dependency(checksum, entry.getKey(), entry.getValue());
214             items.add(dependencyDigest);
215             if (compareWithBaseline) {
216                 dependenciesMatched &= checkItemMatchesBaseline(baselineHolder.get(), dependencyDigest);
217             }
218         }
219 
220         if (compareWithBaseline) {
221             LOGGER.info("Dependencies: {}", dependenciesMatched ? "MATCHED" : "OUT OF DATE");
222         }
223 
224         final ProjectsInputInfo projectsInputInfoType = new ProjectsInputInfo();
225         projectsInputInfoType.setChecksum(checksum.digest());
226         projectsInputInfoType.getItems().addAll(items);
227 
228         final long t2 = System.currentTimeMillis();
229 
230         for (DigestItem item : projectsInputInfoType.getItems()) {
231             LOGGER.debug("Hash calculated, item: {}, hash: {}", item.getType(), item.getHash());
232         }
233         LOGGER.info(
234                 "Project inputs calculated in {} ms. {} checksum [{}] calculated in {} ms.",
235                 t1 - t0,
236                 config.getHashFactory().getAlgorithm(),
237                 projectsInputInfoType.getChecksum(),
238                 t2 - t1);
239         return projectsInputInfoType;
240     }
241 
242     private void checkEffectivePomMatch(ProjectsInputInfo baselineBuild, DigestItem effectivePomChecksum) {
243         Optional<DigestItem> pomHolder = Optional.empty();
244         for (DigestItem it : baselineBuild.getItems()) {
245             if (it.getType().equals("pom")) {
246                 pomHolder = Optional.of(it);
247                 break;
248             }
249         }
250 
251         if (pomHolder.isPresent()) {
252             DigestItem pomItem = pomHolder.get();
253             final boolean matches = StringUtils.equals(pomItem.getHash(), effectivePomChecksum.getHash());
254             if (!matches) {
255                 LOGGER.info(
256                         "Mismatch in effective poms. Current: {}, remote: {}",
257                         effectivePomChecksum.getHash(),
258                         pomItem.getHash());
259             }
260             LOGGER.info("Effective pom: {}", matches ? "MATCHED" : "OUT OF DATE");
261         }
262     }
263 
264     private boolean checkItemMatchesBaseline(ProjectsInputInfo baselineBuild, DigestItem fileDigest) {
265         Optional<DigestItem> baselineFileDigest = Optional.empty();
266         for (DigestItem it : baselineBuild.getItems()) {
267             if (it.getType().equals(fileDigest.getType())
268                     && fileDigest.getValue().equals(it.getValue().trim())) {
269                 baselineFileDigest = Optional.of(it);
270                 break;
271             }
272         }
273 
274         boolean matched = false;
275         if (baselineFileDigest.isPresent()) {
276             String hash = baselineFileDigest.get().getHash();
277             matched = StringUtils.equals(hash, fileDigest.getHash());
278             if (!matched) {
279                 LOGGER.info(
280                         "Mismatch in {}: {}. Local hash: {}, remote: {}",
281                         fileDigest.getType(),
282                         fileDigest.getValue(),
283                         fileDigest.getHash(),
284                         hash);
285             }
286         } else {
287             LOGGER.info("Mismatch in {}: {}. Not found in remote cache", fileDigest.getType(), fileDigest.getValue());
288         }
289         return matched;
290     }
291 
292     /**
293      * @param prototype effective model fully resolved by maven build. Do not pass here just parsed Model.
294      */
295     private String getEffectivePom(Model prototype) throws IOException {
296         ByteArrayOutputStream output = new ByteArrayOutputStream();
297 
298         Writer writer = null;
299         try {
300             writer = WriterFactory.newXmlWriter(output);
301             new MavenXpp3Writer().write(writer, prototype);
302 
303             // normalize env specifics
304             final String[] searchList = {baseDirPath.toString(), "\\", "windows", "linux"};
305             final String[] replacementList = {"", "/", "os.classifier", "os.classifier"};
306 
307             return replaceEachRepeatedly(output.toString(), searchList, replacementList);
308 
309         } finally {
310             IOUtil.close(writer);
311         }
312     }
313 
314     private SortedSet<Path> getInputFiles() {
315         long start = System.currentTimeMillis();
316         HashSet<WalkKey> visitedDirs = new HashSet<>();
317         ArrayList<Path> collectedFiles = new ArrayList<>();
318 
319         org.apache.maven.model.Build build = project.getBuild();
320 
321         final boolean recursive = true;
322         startWalk(Paths.get(build.getSourceDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
323         for (Resource resource : build.getResources()) {
324             startWalk(Paths.get(resource.getDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
325         }
326 
327         startWalk(Paths.get(build.getTestSourceDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
328         for (Resource testResource : build.getTestResources()) {
329             startWalk(Paths.get(testResource.getDirectory()), projectGlob, recursive, collectedFiles, visitedDirs);
330         }
331 
332         Properties properties = project.getProperties();
333         for (String name : properties.stringPropertyNames()) {
334             if (name.startsWith(CACHE_INPUT_NAME)) {
335                 String path = properties.getProperty(name);
336                 startWalk(Paths.get(path), projectGlob, recursive, collectedFiles, visitedDirs);
337             }
338         }
339 
340         List<Include> includes = config.getGlobalIncludePaths();
341         for (Include include : includes) {
342             final String path = include.getValue();
343             final String glob = defaultIfEmpty(include.getGlob(), projectGlob);
344             startWalk(Paths.get(path), glob, include.isRecursive(), collectedFiles, visitedDirs);
345         }
346 
347         long walkKnownPathsFinished = System.currentTimeMillis() - start;
348 
349         LOGGER.info(
350                 "Scanning plugins configurations to find input files. Probing is {}",
351                 processPlugins
352                         ? "enabled, values will be checked for presence in file system"
353                         : "disabled, only tags with attribute " + CACHE_INPUT_NAME + "=\"true\" will be added");
354 
355         if (processPlugins) {
356             collectFromPlugins(collectedFiles, visitedDirs);
357         } else {
358             LOGGER.info("Skipping check plugins scan (probing is disabled by config)");
359         }
360 
361         long pluginsFinished = System.currentTimeMillis() - start - walkKnownPathsFinished;
362 
363         TreeSet<Path> sorted = new TreeSet<>(fileComparator);
364         for (Path collectedFile : collectedFiles) {
365             sorted.add(collectedFile.normalize().toAbsolutePath());
366         }
367 
368         LOGGER.info(
369                 "Found {} input files. Project dir processing: {}, plugins: {} millis",
370                 sorted.size(),
371                 walkKnownPathsFinished,
372                 pluginsFinished);
373         LOGGER.debug("Src input: {}", sorted);
374 
375         return sorted;
376     }
377 
378     private Path convertToAbsolutePath(Path path) {
379         Path resolvedPath = path.isAbsolute() ? path : baseDirPath.resolve(path);
380         return resolvedPath.toAbsolutePath().normalize();
381     }
382 
383     /**
384      * entry point for directory walk
385      */
386     private void startWalk(
387             Path candidate, String glob, boolean recursive, List<Path> collectedFiles, Set<WalkKey> visitedDirs) {
388         Path normalized = convertToAbsolutePath(candidate);
389         WalkKey key = new WalkKey(normalized, glob, recursive);
390         if (visitedDirs.contains(key) || !Files.exists(normalized)) {
391             return;
392         }
393 
394         if (Files.isDirectory(normalized)) {
395             if (baseDirPath.startsWith(normalized)) { // requested to walk parent, can do only non recursive
396                 key = new WalkKey(normalized, glob, false);
397             }
398             try {
399                 walkDir(key, collectedFiles, visitedDirs);
400                 visitedDirs.add(key);
401             } catch (IOException e) {
402                 throw new RuntimeException(e);
403             }
404         } else {
405             if (!exclusionResolver.excludesPath(normalized)) {
406                 LOGGER.debug("Adding: {}", normalized);
407                 collectedFiles.add(normalized);
408             }
409         }
410     }
411 
412     private void collectFromPlugins(List<Path> files, HashSet<WalkKey> visitedDirs) {
413         List<Plugin> plugins = project.getBuild().getPlugins();
414         for (Plugin plugin : plugins) {
415             PluginScanConfig scanConfig = config.getPluginDirScanConfig(plugin);
416 
417             if (scanConfig.isSkip()) {
418                 LOGGER.debug("Skipping plugin config scan (skip by config): {}", plugin.getArtifactId());
419                 continue;
420             }
421 
422             Object configuration = plugin.getConfiguration();
423             LOGGER.debug("Processing plugin config: {}", plugin.getArtifactId());
424             if (configuration != null) {
425                 addInputsFromPluginConfigs(Xpp3DomUtils.getChildren(configuration), scanConfig, files, visitedDirs);
426             }
427 
428             for (PluginExecution exec : plugin.getExecutions()) {
429                 final PluginScanConfig executionScanConfig = config.getExecutionDirScanConfig(plugin, exec);
430                 PluginScanConfig mergedConfig = scanConfig.mergeWith(executionScanConfig);
431 
432                 if (mergedConfig.isSkip()) {
433                     LOGGER.debug(
434                             "Skipping plugin execution config scan (skip by config): {}, execId: {}",
435                             plugin.getArtifactId(),
436                             exec.getId());
437                     continue;
438                 }
439 
440                 Object execConfiguration = exec.getConfiguration();
441                 LOGGER.debug("Processing plugin: {}, execution: {}", plugin.getArtifactId(), exec.getId());
442 
443                 if (execConfiguration != null) {
444                     addInputsFromPluginConfigs(
445                             Xpp3DomUtils.getChildren(execConfiguration), mergedConfig, files, visitedDirs);
446                 }
447             }
448         }
449     }
450 
451     private Path walkDir(final WalkKey key, final List<Path> collectedFiles, final Set<WalkKey> visitedDirs)
452             throws IOException {
453         return Files.walkFileTree(key.getPath(), new SimpleFileVisitor<Path>() {
454 
455             @Override
456             public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes basicFileAttributes)
457                     throws IOException {
458                 WalkKey currentDirKey =
459                         new WalkKey(path.toAbsolutePath().normalize(), key.getGlob(), key.isRecursive());
460                 if (isHidden(path)) {
461                     LOGGER.debug("Skipping subtree (hidden): {}", path);
462                     return FileVisitResult.SKIP_SUBTREE;
463                 } else if (!isReadable(path)) {
464                     LOGGER.debug("Skipping subtree (not readable): {}", path);
465                     return FileVisitResult.SKIP_SUBTREE;
466                 } else if (exclusionResolver.excludesPath(path)) {
467                     LOGGER.debug("Skipping subtree (blacklisted): {}", path);
468                     return FileVisitResult.SKIP_SUBTREE;
469                 } else if (visitedDirs.contains(currentDirKey)) {
470                     LOGGER.debug("Skipping subtree (visited): {}", path);
471                     return FileVisitResult.SKIP_SUBTREE;
472                 }
473 
474                 walkDirectoryFiles(path, collectedFiles, key.getGlob(), entry -> exclusionResolver.excludesPath(entry));
475 
476                 if (!key.isRecursive()) {
477                     LOGGER.debug("Skipping subtree (non recursive): {}", path);
478                     return FileVisitResult.SKIP_SUBTREE;
479                 }
480 
481                 LOGGER.debug("Visiting subtree: {}", path);
482                 return FileVisitResult.CONTINUE;
483             }
484 
485             @Override
486             public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException {
487                 LOGGER.debug("Skipping subtree (exception: {}): {}", exc, path);
488                 return FileVisitResult.SKIP_SUBTREE;
489             }
490         });
491     }
492 
493     private void addInputsFromPluginConfigs(
494             Object[] configurationChildren,
495             PluginScanConfig scanConfig,
496             List<Path> files,
497             HashSet<WalkKey> visitedDirs) {
498         if (configurationChildren == null) {
499             return;
500         }
501 
502         for (Object configChild : configurationChildren) {
503             String tagName = Xpp3DomUtils.getName(configChild);
504             String tagValue = Xpp3DomUtils.getValue(configChild);
505 
506             if (!scanConfig.accept(tagName)) {
507                 LOGGER.debug("Skipping property (scan config)): {}, value: {}", tagName, stripToEmpty(tagValue));
508                 continue;
509             }
510 
511             LOGGER.debug("Checking xml tag. Tag: {}, value: {}", tagName, stripToEmpty(tagValue));
512 
513             addInputsFromPluginConfigs(Xpp3DomUtils.getChildren(configChild), scanConfig, files, visitedDirs);
514 
515             final ScanConfigProperties propertyConfig = scanConfig.getTagScanProperties(tagName);
516             final String glob = defaultIfEmpty(propertyConfig.getGlob(), projectGlob);
517             if ("true".equals(Xpp3DomUtils.getAttribute(configChild, CACHE_INPUT_NAME))) {
518                 LOGGER.info(
519                         "Found tag marked with {} attribute. Tag: {}, value: {}", CACHE_INPUT_NAME, tagName, tagValue);
520                 startWalk(Paths.get(tagValue), glob, propertyConfig.isRecursive(), files, visitedDirs);
521             } else {
522                 final Path candidate = getPathOrNull(tagValue);
523                 if (candidate != null) {
524                     startWalk(candidate, glob, propertyConfig.isRecursive(), files, visitedDirs);
525                     if ("descriptorRef"
526                             .equals(tagName)) { // hardcoded logic for assembly plugin which could reference files
527                         // omitting .xml suffix
528                         startWalk(Paths.get(tagValue + ".xml"), glob, propertyConfig.isRecursive(), files, visitedDirs);
529                     }
530                 }
531             }
532         }
533     }
534 
535     private Path getPathOrNull(String text) {
536         // small optimization to not probe not-paths
537         if (isBlank(text)) {
538             // do not even bother logging about blank/null values
539         } else if (equalsAnyIgnoreCase(text, "true", "false", "utf-8", "null", "\\") // common values
540                 || contains(text, "*") // tag value is a glob or regex - unclear how to process
541                 || (contains(text, ":") && !contains(text, ":\\")) // artifactId
542                 || startsWithAny(text, "com.", "org.", "io.", "java.", "javax.") // java packages
543                 || startsWithAny(text, "${env.") // env variables in maven notation
544                 || startsWithAny(
545                         text,
546                         "http:",
547                         "https:",
548                         "scm:",
549                         "ssh:",
550                         "git:",
551                         "svn:",
552                         "cp:",
553                         "classpath:")) // urls identified by common protocols
554         {
555             LOGGER.debug("Skipping directory (blacklisted literal): {}", text);
556         } else if (startsWithAny(text, tmpDir)) // tmp dir
557         {
558             LOGGER.debug("Skipping directory (temp dir): {}", text);
559         } else {
560             try {
561                 return Paths.get(text);
562             } catch (Exception ignore) {
563                 LOGGER.debug("Skipping directory (invalid path): {}", text);
564             }
565         }
566         return null;
567     }
568 
569     static void walkDirectoryFiles(Path dir, List<Path> collectedFiles, String glob, Predicate<Path> mustBeSkipped) {
570         if (!Files.isDirectory(dir)) {
571             return;
572         }
573 
574         try {
575             try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, glob)) {
576                 for (Path entry : stream) {
577                     if (mustBeSkipped.test(entry)) {
578                         continue;
579                     }
580                     File file = entry.toFile();
581                     if (file.isFile() && !isHidden(entry) && isReadable(entry)) {
582                         collectedFiles.add(entry);
583                     }
584                 }
585             }
586         } catch (IOException e) {
587             throw new RuntimeException("Cannot process directory: " + dir, e);
588         }
589     }
590 
591     private static boolean isHidden(Path entry) throws IOException {
592         return Files.isHidden(entry) || entry.toFile().getName().startsWith(".");
593     }
594 
595     private static boolean isReadable(Path entry) throws IOException {
596         return Files.isReadable(entry);
597     }
598 
599     private SortedMap<String, String> getMutableDependencies() throws IOException {
600         SortedMap<String, String> result = new TreeMap<>();
601 
602         for (Dependency dependency : project.getDependencies()) {
603 
604             if (CacheUtils.isPom(dependency)) {
605                 // POM dependency will be resolved by maven system to actual dependencies
606                 // and will contribute to effective pom.
607                 // Effective result will be recorded by #getNormalizedPom
608                 // so pom dependencies must be skipped as meaningless by themselves
609                 continue;
610             }
611 
612             // saved to index by the end of dependency build
613             MavenProject dependencyProject = multiModuleSupport
614                     .tryToResolveProject(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion())
615                     .orElse(null);
616             boolean isSnapshot = isSnapshot(dependency.getVersion());
617             if (dependencyProject == null && !isSnapshot) {
618                 // external immutable dependency, should skip
619                 continue;
620             }
621             String projectHash;
622             if (dependencyProject != null) // part of multi module
623             {
624                 projectHash =
625                         projectInputCalculator.calculateInput(dependencyProject).getChecksum();
626             } else // this is a snapshot dependency
627             {
628                 DigestItem resolved = resolveArtifact(repoSystem.createDependencyArtifact(dependency), false);
629                 projectHash = resolved.getHash();
630             }
631             result.put(
632                     KeyUtils.getVersionlessArtifactKey(repoSystem.createDependencyArtifact(dependency)), projectHash);
633         }
634         return result;
635     }
636 
637     @Nonnull
638     private DigestItem resolveArtifact(final Artifact dependencyArtifact, boolean isOffline) throws IOException {
639         ArtifactResolutionRequest request = new ArtifactResolutionRequest()
640                 .setArtifact(dependencyArtifact)
641                 .setResolveRoot(true)
642                 .setResolveTransitively(false)
643                 .setLocalRepository(session.getLocalRepository())
644                 .setRemoteRepositories(project.getRemoteArtifactRepositories())
645                 .setOffline(session.isOffline() || isOffline)
646                 .setForceUpdate(session.getRequest().isUpdateSnapshots())
647                 .setServers(session.getRequest().getServers())
648                 .setMirrors(session.getRequest().getMirrors())
649                 .setProxies(session.getRequest().getProxies());
650 
651         final ArtifactResolutionResult result = repoSystem.resolve(request);
652 
653         if (!result.isSuccess()) {
654             throw new DependencyNotResolvedException("Cannot resolve in-project dependency: " + dependencyArtifact);
655         }
656 
657         if (!result.getMissingArtifacts().isEmpty()) {
658             throw new DependencyNotResolvedException(
659                     "Cannot resolve artifact: " + dependencyArtifact + ", missing: " + result.getMissingArtifacts());
660         }
661 
662         if (result.getArtifacts().size() != 1) {
663             throw new IllegalStateException("Unexpected number of artifacts returned. Requested: " + dependencyArtifact
664                     + ", expected: 1, actual: " + result.getArtifacts());
665         }
666 
667         final Artifact resolved = result.getArtifacts().iterator().next();
668 
669         final HashAlgorithm algorithm = config.getHashFactory().createAlgorithm();
670         final String hash = algorithm.hash(resolved.getFile().toPath());
671         return DtoUtils.createDigestedFile(resolved, hash);
672     }
673 
674     /**
675      * PathIgnoringCaseComparator
676      */
677     public static class PathIgnoringCaseComparator implements Comparator<Path> {
678 
679         @Override
680         public int compare(Path f1, Path f2) {
681             String s1 = f1.toAbsolutePath().toString();
682             String s2 = f2.toAbsolutePath().toString();
683             if (File.separator.equals("\\")) {
684                 s1 = s1.replaceAll("\\\\", "/");
685                 s2 = s2.replaceAll("\\\\", "/");
686             }
687             return s1.compareToIgnoreCase(s2);
688         }
689     }
690 
691     /**
692      * Skip lookup on a per-project level via a property to force module rebuild
693      * e.g.{@code <maven.build.cache.skipCache>true<maven.build.cache.skipCache/>}
694      * @param project
695      * @return
696      */
697     public static boolean isSkipCache(MavenProject project) {
698         return Boolean.parseBoolean(project.getProperties().getProperty(CACHE_SKIP, "false"));
699     }
700 
701     /**
702      * Allow skipping generated sources restoration on a per-project level via a property (which defaults to true)
703      * e.g. {@code <maven.build.cache.restoreGeneratedSources>false<maven.build.cache.restoreGeneratedSources/>}.
704      *
705      * @param  project
706      * @return
707      */
708     public static boolean isRestoreGeneratedSources(MavenProject project) {
709         return Boolean.parseBoolean(
710                 project.getProperties().getProperty(RESTORE_GENERATED_SOURCES_PROPERTY_NAME, "true"));
711     }
712 
713     /**
714      * Allow disabling caching entirely on a per-project level via a property - both artifact lookup and upload
715      * Defaults to false
716      * {@code <maven.build.cache.enabled>false<maven.build.cache.enabled/>}
717      * @param project
718      * @return
719      */
720     public static boolean isCacheDisabled(MavenProject project) {
721         return !Boolean.parseBoolean(project.getProperties().getProperty(CACHE_ENABLED_PROPERTY_NAME, "true"));
722     }
723 }