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;
20  
21  import javax.annotation.Nonnull;
22  import javax.inject.Inject;
23  import javax.inject.Named;
24  
25  import java.io.File;
26  import java.io.FileNotFoundException;
27  import java.io.IOException;
28  import java.lang.reflect.Field;
29  import java.lang.reflect.InvocationTargetException;
30  import java.lang.reflect.Method;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.FileVisitResult;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.nio.file.Paths;
36  import java.nio.file.SimpleFileVisitor;
37  import java.nio.file.StandardCopyOption;
38  import java.nio.file.attribute.BasicFileAttributes;
39  import java.util.ArrayList;
40  import java.util.Collections;
41  import java.util.HashMap;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Objects;
45  import java.util.Optional;
46  import java.util.Set;
47  import java.util.TreeSet;
48  import java.util.UUID;
49  import java.util.concurrent.ConcurrentHashMap;
50  import java.util.concurrent.ConcurrentMap;
51  import java.util.concurrent.Future;
52  import java.util.concurrent.FutureTask;
53  import java.util.concurrent.atomic.AtomicBoolean;
54  import java.util.function.Function;
55  import java.util.regex.Pattern;
56  
57  import org.apache.commons.io.FilenameUtils;
58  import org.apache.commons.lang3.StringUtils;
59  import org.apache.commons.lang3.mutable.MutableBoolean;
60  import org.apache.maven.SessionScoped;
61  import org.apache.maven.artifact.handler.ArtifactHandler;
62  import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager;
63  import org.apache.maven.buildcache.artifact.ArtifactRestorationReport;
64  import org.apache.maven.buildcache.artifact.OutputType;
65  import org.apache.maven.buildcache.artifact.RestoredArtifact;
66  import org.apache.maven.buildcache.checksum.MavenProjectInput;
67  import org.apache.maven.buildcache.hash.HashAlgorithm;
68  import org.apache.maven.buildcache.hash.HashFactory;
69  import org.apache.maven.buildcache.xml.Build;
70  import org.apache.maven.buildcache.xml.CacheConfig;
71  import org.apache.maven.buildcache.xml.CacheSource;
72  import org.apache.maven.buildcache.xml.DtoUtils;
73  import org.apache.maven.buildcache.xml.XmlService;
74  import org.apache.maven.buildcache.xml.build.Artifact;
75  import org.apache.maven.buildcache.xml.build.CompletedExecution;
76  import org.apache.maven.buildcache.xml.build.DigestItem;
77  import org.apache.maven.buildcache.xml.build.ProjectsInputInfo;
78  import org.apache.maven.buildcache.xml.build.Scm;
79  import org.apache.maven.buildcache.xml.config.DirName;
80  import org.apache.maven.buildcache.xml.config.PropertyName;
81  import org.apache.maven.buildcache.xml.config.TrackedProperty;
82  import org.apache.maven.buildcache.xml.diff.Diff;
83  import org.apache.maven.buildcache.xml.report.CacheReport;
84  import org.apache.maven.buildcache.xml.report.ProjectReport;
85  import org.apache.maven.execution.MavenSession;
86  import org.apache.maven.execution.MojoExecutionEvent;
87  import org.apache.maven.plugin.MojoExecution;
88  import org.apache.maven.plugin.descriptor.Parameter;
89  import org.apache.maven.project.MavenProject;
90  import org.apache.maven.project.MavenProjectHelper;
91  import org.apache.maven.repository.RepositorySystem;
92  import org.codehaus.plexus.util.ReflectionUtils;
93  import org.slf4j.Logger;
94  import org.slf4j.LoggerFactory;
95  
96  import static java.nio.file.StandardOpenOption.CREATE;
97  import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
98  import static org.apache.commons.lang3.StringUtils.isNotBlank;
99  import static org.apache.commons.lang3.StringUtils.split;
100 import static org.apache.maven.buildcache.CacheResult.empty;
101 import static org.apache.maven.buildcache.CacheResult.failure;
102 import static org.apache.maven.buildcache.CacheResult.partialSuccess;
103 import static org.apache.maven.buildcache.CacheResult.rebuilded;
104 import static org.apache.maven.buildcache.CacheResult.success;
105 import static org.apache.maven.buildcache.RemoteCacheRepository.BUILDINFO_XML;
106 import static org.apache.maven.buildcache.checksum.KeyUtils.getVersionlessProjectKey;
107 import static org.apache.maven.buildcache.checksum.MavenProjectInput.CACHE_IMPLEMENTATION_VERSION;
108 
109 /**
110  * CacheControllerImpl
111  */
112 @SessionScoped
113 @Named
114 @SuppressWarnings("unused")
115 public class CacheControllerImpl implements CacheController {
116 
117     private static final Logger LOGGER = LoggerFactory.getLogger(CacheControllerImpl.class);
118     private static final String DEFAULT_FILE_GLOB = "*";
119     public static final String ERROR_MSG_RESTORATION_OUTSIDE_PROJECT =
120             "Blocked an attempt to restore files outside of a project directory: ";
121 
122     private final MavenProjectHelper projectHelper;
123     private final ArtifactHandlerManager artifactHandlerManager;
124     private final XmlService xmlService;
125     private final CacheConfig cacheConfig;
126     private final LocalCacheRepository localCache;
127     private final RemoteCacheRepository remoteCache;
128     private final ConcurrentMap<String, CacheResult> cacheResults = new ConcurrentHashMap<>();
129     private final LifecyclePhasesHelper lifecyclePhasesHelper;
130     private volatile Map<String, MavenProject> projectIndex;
131     private final ProjectInputCalculator projectInputCalculator;
132     private final RestoredArtifactHandler restoreArtifactHandler;
133     private volatile Scm scm;
134 
135     /**
136      * A map dedicated to store the base path of resources stored to the cache which are not original artifacts
137      * (ex : generated source basedir).
138      * Used to link the resource to its path on disk
139      */
140     private Map<String, Path> attachedResourcesPathsById = new HashMap<>();
141 
142     private int attachedResourceCounter = 0;
143 
144     @Inject
145     public CacheControllerImpl(
146             MavenProjectHelper projectHelper,
147             RepositorySystem repoSystem,
148             ArtifactHandlerManager artifactHandlerManager,
149             XmlService xmlService,
150             LocalCacheRepository localCache,
151             RemoteCacheRepository remoteCache,
152             CacheConfig cacheConfig,
153             ProjectInputCalculator projectInputCalculator,
154             RestoredArtifactHandler restoreArtifactHandler,
155             LifecyclePhasesHelper lifecyclePhasesHelper,
156             MavenSession session) {
157         this.projectHelper = projectHelper;
158         this.localCache = localCache;
159         this.remoteCache = remoteCache;
160         this.cacheConfig = cacheConfig;
161         this.artifactHandlerManager = artifactHandlerManager;
162         this.xmlService = xmlService;
163         this.lifecyclePhasesHelper = lifecyclePhasesHelper;
164         this.projectInputCalculator = projectInputCalculator;
165         this.restoreArtifactHandler = restoreArtifactHandler;
166     }
167 
168     @Override
169     @Nonnull
170     public CacheResult findCachedBuild(
171             MavenSession session, MavenProject project, List<MojoExecution> mojoExecutions, boolean skipCache) {
172         final String highestPhase = lifecyclePhasesHelper.resolveHighestLifecyclePhase(project, mojoExecutions);
173 
174         if (!lifecyclePhasesHelper.isLaterPhaseThanClean(highestPhase)) {
175             return empty();
176         }
177 
178         String projectName = getVersionlessProjectKey(project);
179 
180         ProjectsInputInfo inputInfo = projectInputCalculator.calculateInput(project);
181 
182         final CacheContext context = new CacheContext(project, inputInfo, session);
183 
184         CacheResult result = empty(context);
185         if (!skipCache) {
186 
187             LOGGER.info("Attempting to restore project {} from build cache", projectName);
188 
189             // remote build first
190             if (cacheConfig.isRemoteCacheEnabled()) {
191                 result = findCachedBuild(mojoExecutions, context);
192                 if (!result.isSuccess() && result.getContext() != null) {
193                     LOGGER.info("Remote cache is incomplete or missing, trying local build for {}", projectName);
194                 }
195             }
196 
197             if (!result.isSuccess() && result.getContext() != null) {
198                 CacheResult localBuild = findLocalBuild(mojoExecutions, context);
199                 if (localBuild.isSuccess() || (localBuild.isPartialSuccess() && !result.isPartialSuccess())) {
200                     result = localBuild;
201                 } else {
202                     LOGGER.info(
203                             "Local build was not found by checksum {} for {}", inputInfo.getChecksum(), projectName);
204                 }
205             }
206         } else {
207             LOGGER.info(
208                     "Project {} is marked as requiring force rebuild, will skip lookup in build cache", projectName);
209         }
210         cacheResults.put(getVersionlessProjectKey(project), result);
211 
212         return result;
213     }
214 
215     private CacheResult findCachedBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
216         Optional<Build> cachedBuild = Optional.empty();
217         try {
218             cachedBuild = localCache.findBuild(context);
219             if (cachedBuild.isPresent()) {
220                 return analyzeResult(context, mojoExecutions, cachedBuild.get());
221             }
222         } catch (Exception e) {
223             LOGGER.error("Cannot read cached remote build", e);
224         }
225         return cachedBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
226     }
227 
228     private CacheResult findLocalBuild(List<MojoExecution> mojoExecutions, CacheContext context) {
229         Optional<Build> localBuild = Optional.empty();
230         try {
231             localBuild = localCache.findLocalBuild(context);
232             if (localBuild.isPresent()) {
233                 return analyzeResult(context, mojoExecutions, localBuild.get());
234             }
235         } catch (Exception e) {
236             LOGGER.error("Cannot read local build", e);
237         }
238         return localBuild.map(build -> failure(build, context)).orElseGet(() -> empty(context));
239     }
240 
241     private CacheResult analyzeResult(CacheContext context, List<MojoExecution> mojoExecutions, Build build) {
242         try {
243             final ProjectsInputInfo inputInfo = context.getInputInfo();
244             String projectName = getVersionlessProjectKey(context.getProject());
245 
246             LOGGER.info(
247                     "Found cached build, restoring {} from cache by checksum {}", projectName, inputInfo.getChecksum());
248             LOGGER.debug("Cached build details: {}", build);
249 
250             final String cacheImplementationVersion = build.getCacheImplementationVersion();
251             if (!CACHE_IMPLEMENTATION_VERSION.equals(cacheImplementationVersion)) {
252                 LOGGER.warn(
253                         "Maven and cached build implementations mismatch, caching might not work correctly. "
254                                 + "Implementation version: " + CACHE_IMPLEMENTATION_VERSION + ", cached build: {}",
255                         build.getCacheImplementationVersion());
256             }
257 
258             List<MojoExecution> cachedSegment =
259                     lifecyclePhasesHelper.getCachedSegment(context.getProject(), mojoExecutions, build);
260             List<MojoExecution> missingMojos = build.getMissingExecutions(cachedSegment);
261             if (!missingMojos.isEmpty()) {
262                 LOGGER.warn(
263                         "Cached build doesn't contains all requested plugin executions "
264                                 + "(missing: {}), cannot restore",
265                         missingMojos);
266                 return failure(build, context);
267             }
268 
269             if (!isCachedSegmentPropertiesPresent(context.getProject(), build, cachedSegment)) {
270                 LOGGER.info("Cached build violates cache rules, cannot restore");
271                 return failure(build, context);
272             }
273 
274             final String highestRequestPhase =
275                     lifecyclePhasesHelper.resolveHighestLifecyclePhase(context.getProject(), mojoExecutions);
276 
277             if (lifecyclePhasesHelper.isLaterPhaseThanBuild(highestRequestPhase, build)
278                     && !canIgnoreMissingSegment(context.getProject(), build, mojoExecutions)) {
279                 LOGGER.info(
280                         "Project {} restored partially. Highest cached goal: {}, requested: {}",
281                         projectName,
282                         build.getHighestCompletedGoal(),
283                         highestRequestPhase);
284                 return partialSuccess(build, context);
285             }
286 
287             return success(build, context);
288 
289         } catch (Exception e) {
290             LOGGER.error("Failed to restore project", e);
291             localCache.clearCache(context);
292             return failure(build, context);
293         }
294     }
295 
296     private boolean canIgnoreMissingSegment(MavenProject project, Build info, List<MojoExecution> mojoExecutions) {
297         final List<MojoExecution> postCachedSegment =
298                 lifecyclePhasesHelper.getPostCachedSegment(project, mojoExecutions, info);
299 
300         for (MojoExecution mojoExecution : postCachedSegment) {
301             if (!cacheConfig.canIgnore(mojoExecution)) {
302                 return false;
303             }
304         }
305         return true;
306     }
307 
308     private Function<File, File> createRestorationToDiskConsumer(final MavenProject project, final Artifact artifact) {
309 
310         if (cacheConfig.isRestoreOnDiskArtifacts() && MavenProjectInput.isRestoreOnDiskArtifacts(project)) {
311 
312             Path restorationPath = project.getBasedir().toPath().resolve(artifact.getFilePath());
313             final AtomicBoolean restored = new AtomicBoolean(false);
314             return file -> {
315                 // Set to restored even if it fails later, we don't want multiple try
316                 if (restored.compareAndSet(false, true)) {
317                     verifyRestorationInsideProject(project, restorationPath);
318                     try {
319                         Files.createDirectories(restorationPath.getParent());
320                         Files.copy(file.toPath(), restorationPath, StandardCopyOption.REPLACE_EXISTING);
321                     } catch (IOException e) {
322                         LOGGER.error("Cannot restore file " + artifact.getFileName(), e);
323                         throw new RuntimeException(e);
324                     }
325                     LOGGER.debug("Restored file on disk ({} to {})", artifact.getFileName(), restorationPath);
326                 }
327                 return restorationPath.toFile();
328             };
329         }
330         // Return a consumer doing nothing
331         return file -> file;
332     }
333 
334     private boolean isPathInsideProject(final MavenProject project, Path path) {
335         Path restorationPath = path.toAbsolutePath().normalize();
336         return restorationPath.startsWith(project.getBasedir().toPath());
337     }
338 
339     private void verifyRestorationInsideProject(final MavenProject project, Path path) {
340         if (!isPathInsideProject(project, path)) {
341             Path normalized = path.toAbsolutePath().normalize();
342             LOGGER.error(ERROR_MSG_RESTORATION_OUTSIDE_PROJECT + normalized);
343             throw new RuntimeException(ERROR_MSG_RESTORATION_OUTSIDE_PROJECT + normalized);
344         }
345     }
346 
347     @Override
348     public ArtifactRestorationReport restoreProjectArtifacts(CacheResult cacheResult) {
349 
350         LOGGER.debug("Restore project artifacts");
351         final Build build = cacheResult.getBuildInfo();
352         final CacheContext context = cacheResult.getContext();
353         final MavenProject project = context.getProject();
354         ArtifactRestorationReport restorationReport = new ArtifactRestorationReport();
355 
356         try {
357             RestoredArtifact restoredProjectArtifact = null;
358             List<RestoredArtifact> restoredAttachedArtifacts = new ArrayList<>();
359 
360             if (build.getArtifact() != null && isNotBlank(build.getArtifact().getFileName())) {
361                 final Artifact artifactInfo = build.getArtifact();
362                 String originalVersion = artifactInfo.getVersion();
363                 artifactInfo.setVersion(project.getVersion());
364                 // TODO if remote is forced, probably need to refresh or reconcile all files
365                 final Future<File> downloadTask =
366                         createDownloadTask(cacheResult, context, project, artifactInfo, originalVersion);
367                 restoredProjectArtifact = restoredArtifact(
368                         project.getArtifact(),
369                         artifactInfo.getType(),
370                         artifactInfo.getClassifier(),
371                         downloadTask,
372                         createRestorationToDiskConsumer(project, artifactInfo));
373                 if (!cacheConfig.isLazyRestore()) {
374                     restoredProjectArtifact.getFile();
375                 }
376             }
377 
378             for (Artifact attachedArtifactInfo : build.getAttachedArtifacts()) {
379                 String originalVersion = attachedArtifactInfo.getVersion();
380                 attachedArtifactInfo.setVersion(project.getVersion());
381                 if (isNotBlank(attachedArtifactInfo.getFileName())) {
382                     OutputType outputType = OutputType.fromClassifier(attachedArtifactInfo.getClassifier());
383                     if (OutputType.ARTIFACT != outputType) {
384                         // restoring generated sources / extra output might be unnecessary in CI, could be disabled for
385                         // performance reasons
386                         // it may also be disabled on a per-project level (defaults to true - enable)
387                         if (cacheConfig.isRestoreGeneratedSources()
388                                 && MavenProjectInput.isRestoreGeneratedSources(project)) {
389                             // Set this value before trying the restoration, to keep a trace of the attempt if it fails
390                             restorationReport.setRestoredFilesInProjectDirectory(true);
391                             // generated sources artifact
392                             final Path attachedArtifactFile =
393                                     localCache.getArtifactFile(context, cacheResult.getSource(), attachedArtifactInfo);
394                             restoreGeneratedSources(attachedArtifactInfo, attachedArtifactFile, project);
395                         }
396                     } else {
397                         Future<File> downloadTask = createDownloadTask(
398                                 cacheResult, context, project, attachedArtifactInfo, originalVersion);
399                         final RestoredArtifact restoredAttachedArtifact = restoredArtifact(
400                                 restoredProjectArtifact == null ? project.getArtifact() : restoredProjectArtifact,
401                                 attachedArtifactInfo.getType(),
402                                 attachedArtifactInfo.getClassifier(),
403                                 downloadTask,
404                                 createRestorationToDiskConsumer(project, attachedArtifactInfo));
405                         if (!cacheConfig.isLazyRestore()) {
406                             restoredAttachedArtifact.getFile();
407                         }
408                         restoredAttachedArtifacts.add(restoredAttachedArtifact);
409                     }
410                 }
411             }
412             // Actually modify project at the end in case something went wrong during restoration,
413             // in which case, the project is unmodified and we continue with normal build.
414             if (restoredProjectArtifact != null) {
415                 project.setArtifact(restoredProjectArtifact);
416                 // need to include package lifecycle to save build info for incremental builds
417                 if (!project.hasLifecyclePhase("package")) {
418                     project.addLifecyclePhase("package");
419                 }
420             }
421             restoredAttachedArtifacts.forEach(project::addAttachedArtifact);
422             restorationReport.setSuccess(true);
423         } catch (Exception e) {
424             LOGGER.debug("Cannot restore cache, continuing with normal build.", e);
425         }
426         return restorationReport;
427     }
428 
429     /**
430      * Helper method similar to {@link org.apache.maven.project.MavenProjectHelper#attachArtifact} to work specifically
431      * with restored from cache artifacts
432      */
433     private RestoredArtifact restoredArtifact(
434             org.apache.maven.artifact.Artifact parent,
435             String artifactType,
436             String artifactClassifier,
437             Future<File> artifactFile,
438             Function<File, File> restoreToDiskConsumer) {
439         ArtifactHandler handler = null;
440 
441         if (artifactType != null) {
442             handler = artifactHandlerManager.getArtifactHandler(artifactType);
443         }
444 
445         if (handler == null) {
446             handler = artifactHandlerManager.getArtifactHandler("jar");
447         }
448 
449         // todo: probably need update download url to cache
450         RestoredArtifact artifact = new RestoredArtifact(
451                 parent, artifactFile, artifactType, artifactClassifier, handler, restoreToDiskConsumer);
452         artifact.setResolved(true);
453 
454         return artifact;
455     }
456 
457     private Future<File> createDownloadTask(
458             CacheResult cacheResult,
459             CacheContext context,
460             MavenProject project,
461             Artifact artifact,
462             String originalVersion) {
463         final FutureTask<File> downloadTask = new FutureTask<>(() -> {
464             LOGGER.debug("Downloading artifact {}", artifact.getArtifactId());
465             final Path artifactFile = localCache.getArtifactFile(context, cacheResult.getSource(), artifact);
466 
467             if (!Files.exists(artifactFile)) {
468                 throw new FileNotFoundException("Missing file for cached build, cannot restore. File: " + artifactFile);
469             }
470             LOGGER.debug("Downloaded artifact " + artifact.getArtifactId() + " to: " + artifactFile);
471             return restoreArtifactHandler
472                     .adjustArchiveArtifactVersion(project, originalVersion, artifactFile)
473                     .toFile();
474         });
475         if (!cacheConfig.isLazyRestore()) {
476             downloadTask.run();
477         }
478         return downloadTask;
479     }
480 
481     @Override
482     public void save(
483             CacheResult cacheResult,
484             List<MojoExecution> mojoExecutions,
485             Map<String, MojoExecutionEvent> executionEvents) {
486         CacheContext context = cacheResult.getContext();
487 
488         if (context == null || context.getInputInfo() == null) {
489             LOGGER.info("Cannot save project in cache, skipping");
490             return;
491         }
492 
493         final MavenProject project = context.getProject();
494         final MavenSession session = context.getSession();
495         try {
496             final HashFactory hashFactory = cacheConfig.getHashFactory();
497             final org.apache.maven.artifact.Artifact projectArtifact = project.getArtifact();
498             final List<org.apache.maven.artifact.Artifact> attachedArtifacts;
499             final List<Artifact> attachedArtifactDtos;
500             final Artifact projectArtifactDto;
501             if (project.hasLifecyclePhase("package")) {
502                 final HashAlgorithm algorithm = hashFactory.createAlgorithm();
503                 attachGeneratedSources(project);
504                 attachOutputs(project);
505                 attachedArtifacts = project.getAttachedArtifacts() != null
506                         ? project.getAttachedArtifacts()
507                         : Collections.emptyList();
508                 attachedArtifactDtos = artifactDtos(attachedArtifacts, algorithm, project);
509                 projectArtifactDto = artifactDto(project.getArtifact(), algorithm, project);
510             } else {
511                 attachedArtifacts = Collections.emptyList();
512                 attachedArtifactDtos = new ArrayList<>();
513                 projectArtifactDto = null;
514             }
515 
516             List<CompletedExecution> completedExecution = buildExecutionInfo(mojoExecutions, executionEvents);
517 
518             final Build build = new Build(
519                     session.getGoals(),
520                     projectArtifactDto,
521                     attachedArtifactDtos,
522                     context.getInputInfo(),
523                     completedExecution,
524                     hashFactory.getAlgorithm());
525             populateGitInfo(build, session);
526             build.getDto().set_final(cacheConfig.isSaveToRemoteFinal());
527             cacheResults.put(getVersionlessProjectKey(project), rebuilded(cacheResult, build));
528 
529             localCache.beforeSave(context);
530 
531             // if package phase presence means new artifacts were packaged
532             if (project.hasLifecyclePhase("package")) {
533                 if (projectArtifact.getFile() != null) {
534                     localCache.saveArtifactFile(cacheResult, projectArtifact);
535                 }
536                 for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
537                     if (attachedArtifact.getFile() != null) {
538                         boolean storeArtifact =
539                                 isOutputArtifact(attachedArtifact.getFile().getName());
540                         if (storeArtifact) {
541                             localCache.saveArtifactFile(cacheResult, attachedArtifact);
542                         } else {
543                             LOGGER.debug(
544                                     "Skipping attached project artifact '{}' = "
545                                             + " it is marked for exclusion from caching",
546                                     attachedArtifact.getFile().getName());
547                         }
548                     }
549                 }
550             }
551 
552             localCache.saveBuildInfo(cacheResult, build);
553 
554             if (cacheConfig.isBaselineDiffEnabled()) {
555                 produceDiffReport(cacheResult, build);
556             }
557 
558         } catch (Exception e) {
559             LOGGER.error("Failed to save project, cleaning cache. Project: {}", project, e);
560             try {
561                 localCache.clearCache(context);
562             } catch (Exception ex) {
563                 LOGGER.error("Failed to clean cache due to unexpected error:", ex);
564             }
565         }
566     }
567 
568     public void produceDiffReport(CacheResult cacheResult, Build build) {
569         MavenProject project = cacheResult.getContext().getProject();
570         Optional<Build> baselineHolder = remoteCache.findBaselineBuild(project);
571         if (baselineHolder.isPresent()) {
572             Build baseline = baselineHolder.get();
573             String outputDirectory = project.getBuild().getDirectory();
574             Path reportOutputDir = Paths.get(outputDirectory, "incremental-maven");
575             LOGGER.info("Saving cache builds diff to: {}", reportOutputDir);
576             Diff diff = new CacheDiff(build.getDto(), baseline.getDto(), cacheConfig).compare();
577             try {
578                 Files.createDirectories(reportOutputDir);
579                 final ProjectsInputInfo baselineInputs = baseline.getDto().getProjectsInputInfo();
580                 final String checksum = baselineInputs.getChecksum();
581                 Files.write(
582                         reportOutputDir.resolve("buildinfo-baseline-" + checksum + ".xml"),
583                         xmlService.toBytes(baseline.getDto()),
584                         TRUNCATE_EXISTING,
585                         CREATE);
586                 Files.write(
587                         reportOutputDir.resolve("buildinfo-" + checksum + ".xml"),
588                         xmlService.toBytes(build.getDto()),
589                         TRUNCATE_EXISTING,
590                         CREATE);
591                 Files.write(
592                         reportOutputDir.resolve("buildsdiff-" + checksum + ".xml"),
593                         xmlService.toBytes(diff),
594                         TRUNCATE_EXISTING,
595                         CREATE);
596                 final Optional<DigestItem> pom =
597                         CacheDiff.findPom(build.getDto().getProjectsInputInfo());
598                 if (pom.isPresent()) {
599                     Files.write(
600                             reportOutputDir.resolve("effective-pom-" + checksum + ".xml"),
601                             pom.get().getValue().getBytes(StandardCharsets.UTF_8),
602                             TRUNCATE_EXISTING,
603                             CREATE);
604                 }
605                 final Optional<DigestItem> baselinePom = CacheDiff.findPom(baselineInputs);
606                 if (baselinePom.isPresent()) {
607                     Files.write(
608                             reportOutputDir.resolve("effective-pom-baseline-" + baselineInputs.getChecksum() + ".xml"),
609                             baselinePom.get().getValue().getBytes(StandardCharsets.UTF_8),
610                             TRUNCATE_EXISTING,
611                             CREATE);
612                 }
613             } catch (IOException e) {
614                 LOGGER.error("Cannot produce build diff for project", e);
615             }
616         } else {
617             LOGGER.info("Cannot find project in baseline build, skipping diff");
618         }
619     }
620 
621     private List<Artifact> artifactDtos(
622             List<org.apache.maven.artifact.Artifact> attachedArtifacts, HashAlgorithm digest, MavenProject project)
623             throws IOException {
624         List<Artifact> result = new ArrayList<>();
625         for (org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts) {
626             if (attachedArtifact.getFile() != null
627                     && isOutputArtifact(attachedArtifact.getFile().getName())) {
628                 result.add(artifactDto(attachedArtifact, digest, project));
629             }
630         }
631         return result;
632     }
633 
634     private Artifact artifactDto(
635             org.apache.maven.artifact.Artifact projectArtifact, HashAlgorithm algorithm, MavenProject project)
636             throws IOException {
637         final Artifact dto = DtoUtils.createDto(projectArtifact);
638         if (projectArtifact.getFile() != null && projectArtifact.getFile().isFile()) {
639             final Path file = projectArtifact.getFile().toPath();
640             dto.setFileHash(algorithm.hash(file));
641             dto.setFileSize(Files.size(file));
642 
643             // Get the relative path of any extra zip directory added to the cache
644             Path relativePath = attachedResourcesPathsById.get(projectArtifact.getClassifier());
645             if (relativePath == null) {
646                 // If the path was not a member of this map, we are in presence of an original artifact.
647                 // we get its location on the disk
648                 relativePath = project.getBasedir().toPath().relativize(file.toAbsolutePath());
649             }
650             dto.setFilePath(FilenameUtils.separatorsToUnix(relativePath.toString()));
651         }
652         return dto;
653     }
654 
655     private List<CompletedExecution> buildExecutionInfo(
656             List<MojoExecution> mojoExecutions, Map<String, MojoExecutionEvent> executionEvents) {
657         List<CompletedExecution> list = new ArrayList<>();
658         for (MojoExecution mojoExecution : mojoExecutions) {
659             final String executionKey = CacheUtils.mojoExecutionKey(mojoExecution);
660             final MojoExecutionEvent executionEvent =
661                     executionEvents != null ? executionEvents.get(executionKey) : null;
662             CompletedExecution executionInfo = new CompletedExecution();
663             executionInfo.setExecutionKey(executionKey);
664             executionInfo.setMojoClassName(mojoExecution.getMojoDescriptor().getImplementation());
665             if (executionEvent != null) {
666                 recordMojoProperties(executionInfo, executionEvent);
667             }
668             list.add(executionInfo);
669         }
670         return list;
671     }
672 
673     private void recordMojoProperties(CompletedExecution execution, MojoExecutionEvent executionEvent) {
674         final MojoExecution mojoExecution = executionEvent.getExecution();
675 
676         final boolean logAll = cacheConfig.isLogAllProperties(mojoExecution);
677         List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
678         List<PropertyName> noLogProperties = cacheConfig.getNologProperties(mojoExecution);
679         List<PropertyName> forceLogProperties = cacheConfig.getLoggedProperties(mojoExecution);
680         final Object mojo = executionEvent.getMojo();
681 
682         final File baseDir = executionEvent.getProject().getBasedir();
683         final String baseDirPath = FilenameUtils.normalizeNoEndSeparator(baseDir.getAbsolutePath()) + File.separator;
684 
685         final List<Parameter> parameters = mojoExecution.getMojoDescriptor().getParameters();
686         for (Parameter parameter : parameters) {
687             // editable parameters could be configured by user
688             if (!parameter.isEditable()) {
689                 continue;
690             }
691 
692             final String propertyName = parameter.getName();
693             final boolean tracked = isTracked(propertyName, trackedProperties);
694             if (!tracked && isExcluded(propertyName, logAll, noLogProperties, forceLogProperties)) {
695                 continue;
696             }
697 
698             try {
699                 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(propertyName, mojo.getClass());
700                 if (field != null) {
701                     final Object value = ReflectionUtils.getValueIncludingSuperclasses(propertyName, mojo);
702                     DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
703                     continue;
704                 }
705                 // no field but maybe there is a getter with standard naming and no args
706                 Method getter = getGetter(propertyName, mojo.getClass());
707                 if (getter != null) {
708                     Object value = getter.invoke(mojo);
709                     DtoUtils.addProperty(execution, propertyName, value, baseDirPath, tracked);
710                     continue;
711                 }
712 
713                 if (LOGGER.isWarnEnabled()) {
714                     LOGGER.warn(
715                             "Cannot find a Mojo parameter '{}' to read for Mojo {}. This parameter should be ignored.",
716                             propertyName,
717                             mojoExecution);
718                 }
719 
720             } catch (IllegalAccessException | InvocationTargetException e) {
721                 LOGGER.info("Cannot get property {} value from {}: {}", propertyName, mojo, e.getMessage());
722                 if (tracked) {
723                     throw new IllegalArgumentException("Property configured in cache introspection config " + "for "
724                             + mojo + " is not accessible: " + propertyName);
725                 }
726             }
727         }
728     }
729 
730     private static Method getGetter(String fieldName, Class<?> clazz) {
731         String getterMethodName = "get" + org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter(fieldName);
732         Method[] methods = clazz.getMethods();
733         for (Method method : methods) {
734             if (method.getName().equals(getterMethodName)
735                     && !method.getReturnType().equals(Void.TYPE)
736                     && method.getParameterCount() == 0) {
737                 return method;
738             }
739         }
740         return null;
741     }
742 
743     private boolean isExcluded(
744             String propertyName,
745             boolean logAll,
746             List<PropertyName> excludedProperties,
747             List<PropertyName> forceLogProperties) {
748         if (!forceLogProperties.isEmpty()) {
749             for (PropertyName logProperty : forceLogProperties) {
750                 if (StringUtils.equals(propertyName, logProperty.getPropertyName())) {
751                     return false;
752                 }
753             }
754             return true;
755         }
756 
757         if (!excludedProperties.isEmpty()) {
758             for (PropertyName excludedProperty : excludedProperties) {
759                 if (StringUtils.equals(propertyName, excludedProperty.getPropertyName())) {
760                     return true;
761                 }
762             }
763             return false;
764         }
765 
766         return !logAll;
767     }
768 
769     private boolean isTracked(String propertyName, List<TrackedProperty> trackedProperties) {
770         for (TrackedProperty trackedProperty : trackedProperties) {
771             if (StringUtils.equals(propertyName, trackedProperty.getPropertyName())) {
772                 return true;
773             }
774         }
775         return false;
776     }
777 
778     private boolean isCachedSegmentPropertiesPresent(
779             MavenProject project, Build build, List<MojoExecution> mojoExecutions) {
780         for (MojoExecution mojoExecution : mojoExecutions) {
781             // completion of all mojos checked above, so we expect tp have execution info here
782             final List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties(mojoExecution);
783             final CompletedExecution cachedExecution = build.findMojoExecutionInfo(mojoExecution);
784 
785             if (cachedExecution == null) {
786                 LOGGER.info(
787                         "Execution is not cached. Plugin: {}, goal {}, executionId: {}",
788                         mojoExecution.getPlugin(),
789                         mojoExecution.getGoal(),
790                         mojoExecution.getExecutionId());
791                 return false;
792             }
793 
794             if (!DtoUtils.containsAllProperties(cachedExecution, trackedProperties)) {
795                 LOGGER.warn(
796                         "Cached build record doesn't contain all tracked properties. Plugin: {}, goal: {},"
797                                 + " executionId: {}",
798                         mojoExecution.getPlugin(),
799                         mojoExecution.getGoal(),
800                         mojoExecution.getExecutionId());
801                 return false;
802             }
803         }
804         return true;
805     }
806 
807     @Override
808     public boolean isForcedExecution(MavenProject project, MojoExecution execution) {
809         if (cacheConfig.isForcedExecution(execution)) {
810             return true;
811         }
812 
813         if (StringUtils.isNotBlank(cacheConfig.getAlwaysRunPlugins())) {
814             String[] alwaysRunPluginsList = split(cacheConfig.getAlwaysRunPlugins(), ",");
815             for (String pluginAndGoal : alwaysRunPluginsList) {
816                 String[] tokens = pluginAndGoal.split(":");
817                 String alwaysRunPlugin = tokens[0];
818                 String alwaysRunGoal = tokens.length == 1 ? "*" : tokens[1];
819                 if (Objects.equals(execution.getPlugin().getArtifactId(), alwaysRunPlugin)
820                         && ("*".equals(alwaysRunGoal) || Objects.equals(execution.getGoal(), alwaysRunGoal))) {
821                     return true;
822                 }
823             }
824         }
825         return false;
826     }
827 
828     @Override
829     public void saveCacheReport(MavenSession session) {
830         try {
831             CacheReport cacheReport = new CacheReport();
832             for (CacheResult result : cacheResults.values()) {
833                 ProjectReport projectReport = new ProjectReport();
834                 CacheContext context = result.getContext();
835                 MavenProject project = context.getProject();
836                 projectReport.setGroupId(project.getGroupId());
837                 projectReport.setArtifactId(project.getArtifactId());
838                 projectReport.setChecksum(context.getInputInfo().getChecksum());
839                 boolean checksumMatched = result.getStatus() != RestoreStatus.EMPTY;
840                 projectReport.setChecksumMatched(checksumMatched);
841                 projectReport.setLifecycleMatched(checksumMatched && result.isSuccess());
842                 projectReport.setSource(String.valueOf(result.getSource()));
843                 if (result.getSource() == CacheSource.REMOTE) {
844                     projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
845                 } else if (result.getSource() == CacheSource.BUILD && cacheConfig.isSaveToRemote()) {
846                     projectReport.setSharedToRemote(true);
847                     projectReport.setUrl(remoteCache.getResourceUrl(context, BUILDINFO_XML));
848                 }
849                 cacheReport.addProject(projectReport);
850             }
851 
852             String buildId = UUID.randomUUID().toString();
853             localCache.saveCacheReport(buildId, session, cacheReport);
854         } catch (Exception e) {
855             LOGGER.error("Cannot save incremental build aggregated report", e);
856         }
857     }
858 
859     private void populateGitInfo(Build build, MavenSession session) {
860         if (scm == null) {
861             synchronized (this) {
862                 if (scm == null) {
863                     try {
864                         scm = CacheUtils.readGitInfo(session);
865                     } catch (IOException e) {
866                         scm = new Scm();
867                         LOGGER.error("Cannot populate git info", e);
868                     }
869                 }
870             }
871         }
872         build.getDto().setScm(scm);
873     }
874 
875     private boolean zipAndAttachArtifact(MavenProject project, Path dir, String classifier, final String glob)
876             throws IOException {
877         Path temp = Files.createTempFile("maven-incremental-", project.getArtifactId());
878         temp.toFile().deleteOnExit();
879         boolean hasFile = CacheUtils.zip(dir, temp, glob);
880         if (hasFile) {
881             projectHelper.attachArtifact(project, "zip", classifier, temp.toFile());
882         }
883         return hasFile;
884     }
885 
886     private void restoreGeneratedSources(Artifact artifact, Path artifactFilePath, MavenProject project)
887             throws IOException {
888         final Path baseDir = project.getBasedir().toPath();
889         final Path outputDir = baseDir.resolve(FilenameUtils.separatorsToSystem(artifact.getFilePath()));
890         verifyRestorationInsideProject(project, outputDir);
891         if (!Files.exists(outputDir)) {
892             Files.createDirectories(outputDir);
893         }
894         CacheUtils.unzip(artifactFilePath, outputDir);
895     }
896 
897     // TODO: move to config
898     public void attachGeneratedSources(MavenProject project) throws IOException {
899         final Path targetDir = Paths.get(project.getBuild().getDirectory());
900 
901         final Path generatedSourcesDir = targetDir.resolve("generated-sources");
902         attachDirIfNotEmpty(generatedSourcesDir, targetDir, project, OutputType.GENERATED_SOURCE, DEFAULT_FILE_GLOB);
903 
904         final Path generatedTestSourcesDir = targetDir.resolve("generated-test-sources");
905         attachDirIfNotEmpty(
906                 generatedTestSourcesDir, targetDir, project, OutputType.GENERATED_SOURCE, DEFAULT_FILE_GLOB);
907 
908         Set<String> sourceRoots = new TreeSet<>();
909         if (project.getCompileSourceRoots() != null) {
910             sourceRoots.addAll(project.getCompileSourceRoots());
911         }
912         if (project.getTestCompileSourceRoots() != null) {
913             sourceRoots.addAll(project.getTestCompileSourceRoots());
914         }
915 
916         for (String sourceRoot : sourceRoots) {
917             final Path sourceRootPath = Paths.get(sourceRoot);
918             if (Files.isDirectory(sourceRootPath)
919                     && sourceRootPath.startsWith(targetDir)
920                     && !(sourceRootPath.startsWith(generatedSourcesDir)
921                             || sourceRootPath.startsWith(generatedTestSourcesDir))) { // dir within target
922                 attachDirIfNotEmpty(sourceRootPath, targetDir, project, OutputType.GENERATED_SOURCE, DEFAULT_FILE_GLOB);
923             }
924         }
925     }
926 
927     private void attachOutputs(MavenProject project) throws IOException {
928         final List<DirName> attachedDirs = cacheConfig.getAttachedOutputs();
929         for (DirName dir : attachedDirs) {
930             final Path targetDir = Paths.get(project.getBuild().getDirectory());
931             final Path outputDir = targetDir.resolve(dir.getValue());
932             if (isPathInsideProject(project, outputDir)) {
933                 attachDirIfNotEmpty(outputDir, targetDir, project, OutputType.EXTRA_OUTPUT, dir.getGlob());
934             } else {
935                 LOGGER.warn("Outside project output candidate directory discarded ({})", outputDir.normalize());
936             }
937         }
938     }
939 
940     private void attachDirIfNotEmpty(
941             Path candidateSubDir,
942             Path parentDir,
943             MavenProject project,
944             final OutputType attachedOutputType,
945             final String glob)
946             throws IOException {
947         if (Files.isDirectory(candidateSubDir) && hasFiles(candidateSubDir)) {
948             final Path relativePath = project.getBasedir().toPath().relativize(candidateSubDir);
949             attachedResourceCounter++;
950             final String classifier = attachedOutputType.getClassifierPrefix() + attachedResourceCounter;
951             boolean success = zipAndAttachArtifact(project, candidateSubDir, classifier, glob);
952             if (success) {
953                 attachedResourcesPathsById.put(classifier, relativePath);
954                 LOGGER.debug("Attached directory: {}", candidateSubDir);
955             }
956         }
957     }
958 
959     private boolean hasFiles(Path candidateSubDir) throws IOException {
960         final MutableBoolean hasFiles = new MutableBoolean();
961         Files.walkFileTree(candidateSubDir, new SimpleFileVisitor<Path>() {
962 
963             @Override
964             public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) {
965                 hasFiles.setTrue();
966                 return FileVisitResult.TERMINATE;
967             }
968         });
969         return hasFiles.booleanValue();
970     }
971 
972     private boolean isOutputArtifact(String name) {
973         List<Pattern> excludePatterns = cacheConfig.getExcludePatterns();
974         for (Pattern pattern : excludePatterns) {
975             if (pattern.matcher(name).matches()) {
976                 return false;
977             }
978         }
979         return true;
980     }
981 }