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