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.xml;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  import javax.inject.Inject;
24  import javax.inject.Named;
25  
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.List;
32  import java.util.Optional;
33  import java.util.regex.Pattern;
34  
35  import org.apache.commons.lang3.StringUtils;
36  import org.apache.maven.SessionScoped;
37  import org.apache.maven.buildcache.DefaultPluginScanConfig;
38  import org.apache.maven.buildcache.PluginScanConfig;
39  import org.apache.maven.buildcache.PluginScanConfigImpl;
40  import org.apache.maven.buildcache.hash.HashFactory;
41  import org.apache.maven.buildcache.xml.config.AttachedOutputs;
42  import org.apache.maven.buildcache.xml.config.CacheConfig;
43  import org.apache.maven.buildcache.xml.config.Configuration;
44  import org.apache.maven.buildcache.xml.config.CoordinatesBase;
45  import org.apache.maven.buildcache.xml.config.DirName;
46  import org.apache.maven.buildcache.xml.config.Exclude;
47  import org.apache.maven.buildcache.xml.config.Executables;
48  import org.apache.maven.buildcache.xml.config.ExecutionConfigurationScan;
49  import org.apache.maven.buildcache.xml.config.ExecutionControl;
50  import org.apache.maven.buildcache.xml.config.ExecutionIdsList;
51  import org.apache.maven.buildcache.xml.config.GoalReconciliation;
52  import org.apache.maven.buildcache.xml.config.GoalsList;
53  import org.apache.maven.buildcache.xml.config.Include;
54  import org.apache.maven.buildcache.xml.config.Input;
55  import org.apache.maven.buildcache.xml.config.Local;
56  import org.apache.maven.buildcache.xml.config.MultiModule;
57  import org.apache.maven.buildcache.xml.config.PathSet;
58  import org.apache.maven.buildcache.xml.config.PluginConfigurationScan;
59  import org.apache.maven.buildcache.xml.config.PluginSet;
60  import org.apache.maven.buildcache.xml.config.ProjectVersioning;
61  import org.apache.maven.buildcache.xml.config.PropertyName;
62  import org.apache.maven.buildcache.xml.config.Remote;
63  import org.apache.maven.buildcache.xml.config.TrackedProperty;
64  import org.apache.maven.execution.MavenSession;
65  import org.apache.maven.model.Plugin;
66  import org.apache.maven.model.PluginExecution;
67  import org.apache.maven.plugin.MojoExecution;
68  import org.apache.maven.rtinfo.RuntimeInformation;
69  import org.slf4j.Logger;
70  import org.slf4j.LoggerFactory;
71  
72  import static java.lang.Boolean.TRUE;
73  import static org.apache.maven.buildcache.CacheUtils.getMultimoduleRoot;
74  
75  /**
76   * CacheConfigImpl
77   */
78  @SessionScoped
79  @Named
80  @SuppressWarnings("unused")
81  public class CacheConfigImpl implements org.apache.maven.buildcache.xml.CacheConfig {
82  
83      public static final String CONFIG_PATH_PROPERTY_NAME = "maven.build.cache.configPath";
84      public static final String CACHE_ENABLED_PROPERTY_NAME = "maven.build.cache.enabled";
85      public static final String CACHE_LOCATION_PROPERTY_NAME = "maven.build.cache.location";
86      public static final String REMOTE_ENABLED_PROPERTY_NAME = "maven.build.cache.remote.enabled";
87      public static final String REMOTE_URL_PROPERTY_NAME = "maven.build.cache.remote.url";
88      public static final String REMOTE_SERVER_ID_PROPERTY_NAME = "maven.build.cache.remote.server.id";
89      public static final String SAVE_TO_REMOTE_PROPERTY_NAME = "maven.build.cache.remote.save.enabled";
90      public static final String SAVE_NON_OVERRIDEABLE_NAME = "maven.build.cache.remote.save.final";
91      public static final String FAIL_FAST_PROPERTY_NAME = "maven.build.cache.failFast";
92      public static final String BASELINE_BUILD_URL_PROPERTY_NAME = "maven.build.cache.baselineUrl";
93      public static final String LAZY_RESTORE_PROPERTY_NAME = "maven.build.cache.lazyRestore";
94      public static final String RESTORE_ON_DISK_ARTIFACTS_PROPERTY_NAME = "maven.build.cache.restoreOnDiskArtifacts";
95      public static final String RESTORE_GENERATED_SOURCES_PROPERTY_NAME = "maven.build.cache.restoreGeneratedSources";
96      public static final String ALWAYS_RUN_PLUGINS = "maven.build.cache.alwaysRunPlugins";
97      public static final String MANDATORY_CLEAN = "maven.build.cache.mandatoryClean";
98  
99      /**
100      * Flag to control if we should skip lookup for cached artifacts globally or for a particular project even if
101      * qualifying artifacts exist in build cache.
102      * E.g. to trigger a forced build (full or for a particular module)
103      * May be also activated via properties for projects via a profile e.g. on CI when some files produced by the build
104      * are required (e.g. smth. from target folder as additional CI build artifacts):
105      * {@code <maven.build.cache.skipCache>true<maven.build.cache.skipCache/>}
106      */
107     public static final String CACHE_SKIP = "maven.build.cache.skipCache";
108 
109     /**
110      * Flag to disable cache saving
111      */
112     public static final String SKIP_SAVE = "maven.build.cache.skipSave";
113 
114     private static final Logger LOGGER = LoggerFactory.getLogger(CacheConfigImpl.class);
115 
116     private final XmlService xmlService;
117     private final MavenSession session;
118     private final RuntimeInformation rtInfo;
119 
120     private volatile CacheState state;
121     private CacheConfig cacheConfig;
122     private HashFactory hashFactory;
123     private List<Pattern> excludePatterns;
124 
125     @Inject
126     public CacheConfigImpl(XmlService xmlService, MavenSession session, RuntimeInformation rtInfo) {
127         this.xmlService = xmlService;
128         this.session = session;
129         this.rtInfo = rtInfo;
130     }
131 
132     @Nonnull
133     @Override
134     public CacheState initialize() {
135         if (state == null) {
136             synchronized (this) {
137                 if (state == null) {
138                     final boolean enabled = getProperty(CACHE_ENABLED_PROPERTY_NAME, true);
139 
140                     if (!rtInfo.isMavenVersion("[3.9.0,)")) {
141                         LOGGER.warn("Cache requires Maven >= 3.9, but version is " + rtInfo.getMavenVersion()
142                                 + ". Disabling cache.");
143                         state = CacheState.DISABLED;
144                     } else if (!enabled) {
145                         LOGGER.info("Cache disabled by command line flag, project will be built fully and not cached");
146                         state = CacheState.DISABLED;
147                     } else {
148                         Path configPath;
149 
150                         String configPathText = getProperty(CONFIG_PATH_PROPERTY_NAME, null);
151                         if (StringUtils.isNotBlank(configPathText)) {
152                             configPath = Paths.get(configPathText);
153                         } else {
154                             configPath =
155                                     getMultimoduleRoot(session).resolve(".mvn").resolve("maven-build-cache-config.xml");
156                         }
157 
158                         if (!Files.exists(configPath)) {
159                             LOGGER.info(
160                                     "Cache configuration is not available at configured path {}, "
161                                             + "cache is enabled with defaults",
162                                     configPath);
163                             cacheConfig = new CacheConfig();
164                         } else {
165                             try {
166                                 LOGGER.info("Loading cache configuration from {}", configPath);
167                                 cacheConfig = xmlService.loadCacheConfig(configPath.toFile());
168                             } catch (Exception e) {
169                                 throw new IllegalArgumentException(
170                                         "Cannot initialize cache because xml config is not valid or not available", e);
171                             }
172                         }
173                         fillWithDefaults(cacheConfig);
174 
175                         // `maven.build.cache.enabled` overrides the `enabled` of the XML file
176                         // to allow a disabled configuration to be enabled on the command line
177                         boolean cacheEnabled = getProperty(
178                                 CACHE_ENABLED_PROPERTY_NAME, getConfiguration().isEnabled());
179 
180                         if (!cacheEnabled) {
181                             state = CacheState.DISABLED;
182                         } else {
183                             String hashAlgorithm = null;
184                             try {
185                                 hashAlgorithm = getConfiguration().getHashAlgorithm();
186                                 hashFactory = HashFactory.of(hashAlgorithm);
187                                 LOGGER.info("Using {} hash algorithm for cache", hashAlgorithm);
188                             } catch (Exception e) {
189                                 throw new IllegalArgumentException(
190                                         "Unsupported hashing algorithm: " + hashAlgorithm, e);
191                             }
192 
193                             excludePatterns = compileExcludePatterns();
194                             state = CacheState.INITIALIZED;
195                         }
196                     }
197                 }
198             }
199         }
200         return state;
201     }
202 
203     private void fillWithDefaults(CacheConfig cacheConfig) {
204         if (cacheConfig.getConfiguration() == null) {
205             cacheConfig.setConfiguration(new Configuration());
206         }
207         Configuration configuration = cacheConfig.getConfiguration();
208         if (configuration.getLocal() == null) {
209             configuration.setLocal(new Local());
210         }
211         if (configuration.getRemote() == null) {
212             configuration.setRemote(new Remote());
213         }
214         if (cacheConfig.getInput() == null) {
215             cacheConfig.setInput(new Input());
216         }
217         Input input = cacheConfig.getInput();
218         if (input.getGlobal() == null) {
219             input.setGlobal(new PathSet());
220         }
221     }
222 
223     @Nonnull
224     @Override
225     public List<TrackedProperty> getTrackedProperties(MojoExecution mojoExecution) {
226         checkInitializedState();
227         final GoalReconciliation reconciliationConfig = findReconciliationConfig(mojoExecution);
228         if (reconciliationConfig != null) {
229             return reconciliationConfig.getReconciles();
230         } else {
231             return Collections.emptyList();
232         }
233     }
234 
235     @Override
236     public boolean isLogAllProperties(MojoExecution mojoExecution) {
237         final GoalReconciliation reconciliationConfig = findReconciliationConfig(mojoExecution);
238         if (reconciliationConfig != null && reconciliationConfig.isLogAll()) {
239             return true;
240         }
241         return cacheConfig.getExecutionControl() != null
242                 && cacheConfig.getExecutionControl().getReconcile() != null
243                 && cacheConfig.getExecutionControl().getReconcile().isLogAllProperties();
244     }
245 
246     private GoalReconciliation findReconciliationConfig(MojoExecution mojoExecution) {
247         if (cacheConfig.getExecutionControl() == null) {
248             return null;
249         }
250 
251         final ExecutionControl executionControl = cacheConfig.getExecutionControl();
252         if (executionControl.getReconcile() == null) {
253             return null;
254         }
255 
256         final List<GoalReconciliation> reconciliation =
257                 executionControl.getReconcile().getPlugins();
258 
259         for (GoalReconciliation goalReconciliationConfig : reconciliation) {
260             final String goal = mojoExecution.getGoal();
261 
262             if (isPluginMatch(mojoExecution.getPlugin(), goalReconciliationConfig)
263                     && StringUtils.equals(goal, goalReconciliationConfig.getGoal())) {
264                 return goalReconciliationConfig;
265             }
266         }
267         return null;
268     }
269 
270     @Nonnull
271     @Override
272     public List<PropertyName> getLoggedProperties(MojoExecution mojoExecution) {
273         checkInitializedState();
274 
275         final GoalReconciliation reconciliationConfig = findReconciliationConfig(mojoExecution);
276         if (reconciliationConfig != null) {
277             return reconciliationConfig.getLogs();
278         } else {
279             return Collections.emptyList();
280         }
281     }
282 
283     @Nonnull
284     @Override
285     public List<PropertyName> getNologProperties(MojoExecution mojoExecution) {
286         checkInitializedState();
287         final GoalReconciliation reconciliationConfig = findReconciliationConfig(mojoExecution);
288         if (reconciliationConfig != null) {
289             return reconciliationConfig.getNologs();
290         } else {
291             return Collections.emptyList();
292         }
293     }
294 
295     @Nonnull
296     @Override
297     public List<String> getEffectivePomExcludeProperties(Plugin plugin) {
298         checkInitializedState();
299         final PluginConfigurationScan pluginConfig = findPluginScanConfig(plugin);
300 
301         if (pluginConfig != null && pluginConfig.getEffectivePom() != null) {
302             return pluginConfig.getEffectivePom().getExcludeProperties();
303         }
304         return Collections.emptyList();
305     }
306 
307     @Nullable
308     @Override
309     public MultiModule getMultiModule() {
310         checkInitializedState();
311         return cacheConfig.getConfiguration().getMultiModule();
312     }
313 
314     private PluginConfigurationScan findPluginScanConfig(Plugin plugin) {
315         if (cacheConfig.getInput() == null) {
316             return null;
317         }
318 
319         final List<PluginConfigurationScan> pluginConfigs =
320                 cacheConfig.getInput().getPlugins();
321         for (PluginConfigurationScan pluginConfig : pluginConfigs) {
322             if (isPluginMatch(plugin, pluginConfig)) {
323                 return pluginConfig;
324             }
325         }
326         return null;
327     }
328 
329     private boolean isPluginMatch(Plugin plugin, CoordinatesBase pluginConfig) {
330         return StringUtils.equals(pluginConfig.getArtifactId(), plugin.getArtifactId())
331                 && (pluginConfig.getGroupId() == null
332                         || StringUtils.equals(pluginConfig.getGroupId(), plugin.getGroupId()));
333     }
334 
335     @Nonnull
336     @Override
337     public PluginScanConfig getPluginDirScanConfig(Plugin plugin) {
338         checkInitializedState();
339         final PluginConfigurationScan pluginConfig = findPluginScanConfig(plugin);
340         if (pluginConfig == null || pluginConfig.getDirScan() == null) {
341             return new DefaultPluginScanConfig();
342         }
343 
344         return new PluginScanConfigImpl(pluginConfig.getDirScan());
345     }
346 
347     @Nonnull
348     @Override
349     public PluginScanConfig getExecutionDirScanConfig(Plugin plugin, PluginExecution exec) {
350         checkInitializedState();
351         final PluginConfigurationScan pluginScanConfig = findPluginScanConfig(plugin);
352 
353         if (pluginScanConfig != null) {
354             final ExecutionConfigurationScan executionScanConfig =
355                     findExecutionScanConfig(exec, pluginScanConfig.getExecutions());
356             if (executionScanConfig != null && executionScanConfig.getDirScan() != null) {
357                 return new PluginScanConfigImpl(executionScanConfig.getDirScan());
358             }
359         }
360 
361         return new DefaultPluginScanConfig();
362     }
363 
364     private ExecutionConfigurationScan findExecutionScanConfig(
365             PluginExecution execution, List<ExecutionConfigurationScan> scanConfigs) {
366         for (ExecutionConfigurationScan executionScanConfig : scanConfigs) {
367             if (executionScanConfig.getExecIds().contains(execution.getId())) {
368                 return executionScanConfig;
369             }
370         }
371         return null;
372     }
373 
374     @Override
375     public String isProcessPlugins() {
376         checkInitializedState();
377         return TRUE.toString();
378     }
379 
380     @Override
381     public String getDefaultGlob() {
382         checkInitializedState();
383         return StringUtils.trim(cacheConfig.getInput().getGlobal().getGlob());
384     }
385 
386     @Nonnull
387     @Override
388     public List<Include> getGlobalIncludePaths() {
389         checkInitializedState();
390         return cacheConfig.getInput().getGlobal().getIncludes();
391     }
392 
393     @Nonnull
394     @Override
395     public List<Exclude> getGlobalExcludePaths() {
396         checkInitializedState();
397         return cacheConfig.getInput().getGlobal().getExcludes();
398     }
399 
400     @Nonnull
401     @Override
402     public HashFactory getHashFactory() {
403         checkInitializedState();
404         return hashFactory;
405     }
406 
407     @Override
408     public boolean canIgnore(MojoExecution mojoExecution) {
409         checkInitializedState();
410         if (cacheConfig.getExecutionControl() == null
411                 || cacheConfig.getExecutionControl().getIgnoreMissing() == null) {
412             return false;
413         }
414 
415         return executionMatches(mojoExecution, cacheConfig.getExecutionControl().getIgnoreMissing());
416     }
417 
418     @Override
419     public boolean isForcedExecution(MojoExecution execution) {
420         checkInitializedState();
421         if (cacheConfig.getExecutionControl() == null
422                 || cacheConfig.getExecutionControl().getRunAlways() == null) {
423             return false;
424         }
425 
426         return executionMatches(execution, cacheConfig.getExecutionControl().getRunAlways());
427     }
428 
429     private boolean executionMatches(MojoExecution execution, Executables executablesType) {
430         final List<PluginSet> pluginConfigs = executablesType.getPlugins();
431         for (PluginSet pluginConfig : pluginConfigs) {
432             if (isPluginMatch(execution.getPlugin(), pluginConfig)) {
433                 return true;
434             }
435         }
436 
437         final List<ExecutionIdsList> executionIds = executablesType.getExecutions();
438         for (ExecutionIdsList executionConfig : executionIds) {
439             if (isPluginMatch(execution.getPlugin(), executionConfig)
440                     && executionConfig.getExecIds().contains(execution.getExecutionId())) {
441                 return true;
442             }
443         }
444 
445         final List<GoalsList> pluginsGoalsList = executablesType.getGoalsLists();
446         for (GoalsList pluginGoals : pluginsGoalsList) {
447             if (isPluginMatch(execution.getPlugin(), pluginGoals)
448                     && pluginGoals.getGoals().contains(execution.getGoal())) {
449                 return true;
450             }
451         }
452 
453         return false;
454     }
455 
456     @Override
457     public boolean isEnabled() {
458         return state == CacheState.INITIALIZED;
459     }
460 
461     @Override
462     public boolean isRemoteCacheEnabled() {
463         checkInitializedState();
464         return getUrl() != null
465                 && getProperty(REMOTE_ENABLED_PROPERTY_NAME, getRemote().isEnabled());
466     }
467 
468     @Override
469     public boolean isSaveToRemote() {
470         return isRemoteCacheEnabled()
471                 && getProperty(SAVE_TO_REMOTE_PROPERTY_NAME, getRemote().isSaveToRemote());
472     }
473 
474     @Override
475     public boolean isSaveToRemoteFinal() {
476         return isSaveToRemote() && getProperty(SAVE_NON_OVERRIDEABLE_NAME, false);
477     }
478 
479     @Override
480     public boolean isSkipCache() {
481         return getProperty(CACHE_SKIP, false);
482     }
483 
484     @Override
485     public boolean isFailFast() {
486         return getProperty(FAIL_FAST_PROPERTY_NAME, false);
487     }
488 
489     @Override
490     public boolean isBaselineDiffEnabled() {
491         return getProperty(BASELINE_BUILD_URL_PROPERTY_NAME, null) != null;
492     }
493 
494     @Override
495     public String getBaselineCacheUrl() {
496         return getProperty(BASELINE_BUILD_URL_PROPERTY_NAME, null);
497     }
498 
499     @Override
500     public boolean isLazyRestore() {
501         return getProperty(LAZY_RESTORE_PROPERTY_NAME, false);
502     }
503 
504     @Override
505     public boolean isRestoreGeneratedSources() {
506         return getProperty(RESTORE_GENERATED_SOURCES_PROPERTY_NAME, true);
507     }
508 
509     @Override
510     public boolean isRestoreOnDiskArtifacts() {
511         return getProperty(RESTORE_ON_DISK_ARTIFACTS_PROPERTY_NAME, true);
512     }
513 
514     @Override
515     public String getAlwaysRunPlugins() {
516         return getProperty(ALWAYS_RUN_PLUGINS, null);
517     }
518 
519     @Override
520     public boolean isSkipSave() {
521         return getProperty(SKIP_SAVE, false);
522     }
523 
524     @Override
525     public boolean isMandatoryClean() {
526         return getProperty(MANDATORY_CLEAN, getConfiguration().isMandatoryClean());
527     }
528 
529     @Override
530     public String getId() {
531         checkInitializedState();
532         return getProperty(REMOTE_SERVER_ID_PROPERTY_NAME, getRemote().getId());
533     }
534 
535     @Override
536     public String getUrl() {
537         checkInitializedState();
538         return getProperty(REMOTE_URL_PROPERTY_NAME, getRemote().getUrl());
539     }
540 
541     @Override
542     public String getTransport() {
543         checkInitializedState();
544         return getRemote().getTransport();
545     }
546 
547     @Override
548     public int getMaxLocalBuildsCached() {
549         checkInitializedState();
550         return getLocal().getMaxBuildsCached();
551     }
552 
553     @Override
554     public String getLocalRepositoryLocation() {
555         checkInitializedState();
556         return getProperty(CACHE_LOCATION_PROPERTY_NAME, getLocal().getLocation());
557     }
558 
559     @Override
560     public List<DirName> getAttachedOutputs() {
561         checkInitializedState();
562         final AttachedOutputs attachedOutputs = getConfiguration().getAttachedOutputs();
563         return attachedOutputs == null ? Collections.emptyList() : attachedOutputs.getDirNames();
564     }
565 
566     @Override
567     public boolean adjustMetaInfVersion() {
568         if (isEnabled()) {
569             return Optional.ofNullable(getConfiguration().getProjectVersioning())
570                     .map(ProjectVersioning::isAdjustMetaInf)
571                     .orElse(false);
572         } else {
573             return false;
574         }
575     }
576 
577     @Override
578     public boolean calculateProjectVersionChecksum() {
579         if (isEnabled()) {
580             return Optional.ofNullable(getConfiguration().getProjectVersioning())
581                     .map(ProjectVersioning::isCalculateProjectVersionChecksum)
582                     .orElse(false);
583         } else {
584             return false;
585         }
586     }
587 
588     @Nonnull
589     @Override
590     public List<Pattern> getExcludePatterns() {
591         checkInitializedState();
592         return excludePatterns;
593     }
594 
595     private List<Pattern> compileExcludePatterns() {
596         if (cacheConfig.getOutput() != null && cacheConfig.getOutput().getExclude() != null) {
597             List<Pattern> patterns = new ArrayList<>();
598             for (String pattern : cacheConfig.getOutput().getExclude().getPatterns()) {
599                 patterns.add(Pattern.compile(pattern));
600             }
601             return patterns;
602         }
603         return Collections.emptyList();
604     }
605 
606     private Remote getRemote() {
607         return getConfiguration().getRemote();
608     }
609 
610     private Local getLocal() {
611         return getConfiguration().getLocal();
612     }
613 
614     private Configuration getConfiguration() {
615         return cacheConfig.getConfiguration();
616     }
617 
618     private void checkInitializedState() {
619         if (state != CacheState.INITIALIZED) {
620             throw new IllegalStateException("Cache is not initialized. Actual state: " + state);
621         }
622     }
623 
624     private String getProperty(String key, String defaultValue) {
625         String value = session.getUserProperties().getProperty(key);
626         if (value == null) {
627             value = session.getSystemProperties().getProperty(key);
628             if (value == null) {
629                 value = defaultValue;
630             }
631         }
632         return value;
633     }
634 
635     private boolean getProperty(String key, boolean defaultValue) {
636         String value = session.getUserProperties().getProperty(key);
637         if (value == null) {
638             value = session.getSystemProperties().getProperty(key);
639             if (value == null) {
640                 return defaultValue;
641             }
642         }
643         return Boolean.parseBoolean(value);
644     }
645 }