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