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