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.plugins.invoker;
20  
21  import java.io.File;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Optional;
29  import java.util.Properties;
30  import java.util.function.Consumer;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.maven.shared.invoker.InvocationRequest;
36  
37  /**
38   * Provides a convenient facade around the <code>invoker.properties</code>.
39   *
40   * @author Benjamin Bentmann
41   */
42  class InvokerProperties {
43      private static final String SELECTOR_PREFIX = "selector.";
44  
45      private static final Pattern ENVIRONMENT_VARIABLES_PATTERN =
46              Pattern.compile("invoker\\.environmentVariables\\.([A-Za-z][^.]+)(\\.([0-9]+))?");
47  
48      // default values from Mojo configuration
49      private Boolean defaultDebug;
50      private Boolean defaultQuiet;
51      private List<String> defaultGoals;
52      private List<String> defaultProfiles;
53      private String defaultMavenOpts;
54      private Integer defaultTimeoutInSeconds;
55      private Map<String, String> defaultEnvironmentVariables;
56      private File defaultMavenExecutable;
57      private Boolean defaultUpdateSnapshots;
58  
59      private enum InvocationProperty {
60          PROJECT("invoker.project"),
61          BUILD_RESULT("invoker.buildResult"),
62          GOALS("invoker.goals"),
63          PROFILES("invoker.profiles"),
64          MAVEN_EXECUTABLE("invoker.mavenExecutable"),
65          MAVEN_OPTS("invoker.mavenOpts"),
66          FAILURE_BEHAVIOR("invoker.failureBehavior"),
67          NON_RECURSIVE("invoker.nonRecursive"),
68          OFFLINE("invoker.offline"),
69          SYSTEM_PROPERTIES_FILE("invoker.systemPropertiesFile"),
70          DEBUG("invoker.debug"),
71          QUIET("invoker.quiet"),
72          SETTINGS_FILE("invoker.settingsFile"),
73          TIMEOUT_IN_SECONDS("invoker.timeoutInSeconds"),
74          UPDATE_SNAPSHOTS("invoker.updateSnapshots");
75  
76          private final String key;
77  
78          InvocationProperty(final String s) {
79              this.key = s;
80          }
81  
82          @Override
83          public String toString() {
84              return key;
85          }
86      }
87  
88      private enum SelectorProperty {
89          JAVA_VERSION(".java.version"),
90          MAVEN_VERSION(".maven.version"),
91          OS_FAMLY(".os.family");
92  
93          private final String suffix;
94  
95          SelectorProperty(String suffix) {
96              this.suffix = suffix;
97          }
98  
99          @Override
100         public String toString() {
101             return suffix;
102         }
103     }
104 
105     /**
106      * The invoker properties being wrapped.
107      */
108     private final Properties properties;
109 
110     /**
111      * Creates a new facade for the specified invoker properties. The properties will not be copied, so any changes to
112      * them will be reflected by the facade.
113      *
114      * @param properties The invoker properties to wrap, may be <code>null</code> if none.
115      */
116     InvokerProperties(Properties properties) {
117         this.properties = (properties != null) ? properties : new Properties();
118     }
119 
120     /**
121      * Default value for debug
122      * @param defaultDebug a default value
123      */
124     public void setDefaultDebug(boolean defaultDebug) {
125         this.defaultDebug = defaultDebug;
126     }
127 
128     /**
129      * Default value for quiet
130      * @param defaultQuiet a default value
131      */
132     public void setDefaultQuiet(boolean defaultQuiet) {
133         this.defaultQuiet = defaultQuiet;
134     }
135 
136     /**
137      * Default value for goals
138      * @param defaultGoals a default value
139      */
140     public void setDefaultGoals(List<String> defaultGoals) {
141         this.defaultGoals = defaultGoals;
142     }
143 
144     /**
145      * Default value for profiles
146      * @param defaultProfiles a default value
147      */
148     public void setDefaultProfiles(List<String> defaultProfiles) {
149         this.defaultProfiles = defaultProfiles;
150     }
151 
152     /**
153      * Default value for mavenExecutable
154      * @param defaultMavenExecutable a default value
155      */
156     public void setDefaultMavenExecutable(String defaultMavenExecutable) {
157         if (StringUtils.isNotBlank(defaultMavenExecutable)) {
158             this.defaultMavenExecutable = new File(defaultMavenExecutable);
159         }
160     }
161 
162     /**
163      * Default value for mavenOpts
164      * @param defaultMavenOpts a default value
165      */
166     public void setDefaultMavenOpts(String defaultMavenOpts) {
167         this.defaultMavenOpts = defaultMavenOpts;
168     }
169 
170     /**
171      * Default value for timeoutInSeconds
172      * @param defaultTimeoutInSeconds a default value
173      */
174     public void setDefaultTimeoutInSeconds(int defaultTimeoutInSeconds) {
175         this.defaultTimeoutInSeconds = defaultTimeoutInSeconds;
176     }
177 
178     /**
179      * Default value for environmentVariables
180      * @param defaultEnvironmentVariables a default value
181      */
182     public void setDefaultEnvironmentVariables(Map<String, String> defaultEnvironmentVariables) {
183         this.defaultEnvironmentVariables = defaultEnvironmentVariables;
184     }
185 
186     /**
187      * Default value for updateSnapshots
188      * @param defaultUpdateSnapshots a default value
189      */
190     public void setDefaultUpdateSnapshots(boolean defaultUpdateSnapshots) {
191         this.defaultUpdateSnapshots = defaultUpdateSnapshots;
192     }
193 
194     /**
195      * Gets the invoker properties being wrapped.
196      *
197      * @return The invoker properties being wrapped, never <code>null</code>.
198      */
199     public Properties getProperties() {
200         return this.properties;
201     }
202 
203     /**
204      * Gets the name of the corresponding build job.
205      *
206      * @return The name of the build job or an empty string if not set.
207      */
208     public String getJobName() {
209         return this.properties.getProperty("invoker.name", "");
210     }
211 
212     /**
213      * Gets the description of the corresponding build job.
214      *
215      * @return The description of the build job or an empty string if not set.
216      */
217     public String getJobDescription() {
218         return this.properties.getProperty("invoker.description", "");
219     }
220 
221     /**
222      * Get the corresponding ordinal value
223      *
224      * @return The ordinal value
225      */
226     public int getOrdinal() {
227         return Integer.parseInt(this.properties.getProperty("invoker.ordinal", "0"));
228     }
229 
230     /**
231      * Gets the specification of JRE versions on which this build job should be run.
232      *
233      * @return The specification of JRE versions or an empty string if not set.
234      */
235     public String getJreVersion() {
236         return this.properties.getProperty("invoker.java.version", "");
237     }
238 
239     /**
240      * Gets the specification of JRE versions on which this build job should be run.
241      *
242      * @return The specification of JRE versions or an empty string if not set.
243      */
244     public String getJreVersion(int index) {
245         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.JAVA_VERSION, getJreVersion());
246     }
247 
248     /**
249      * Gets the specification of Maven versions on which this build job should be run.
250      *
251      * @return The specification of Maven versions on which this build job should be run.
252      * @since 1.5
253      */
254     public String getMavenVersion() {
255         return this.properties.getProperty("invoker.maven.version", "");
256     }
257 
258     /**
259      * @param index the selector index
260      * @return The specification of Maven versions on which this build job should be run.
261      * @since 3.0.0
262      */
263     public String getMavenVersion(int index) {
264         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.MAVEN_VERSION, getMavenVersion());
265     }
266 
267     /**
268      * Gets the specification of OS families on which this build job should be run.
269      *
270      * @return The specification of OS families or an empty string if not set.
271      */
272     public String getOsFamily() {
273         return this.properties.getProperty("invoker.os.family", "");
274     }
275 
276     /**
277      * Gets the specification of OS families on which this build job should be run.
278      *
279      * @param index the selector index
280      * @return The specification of OS families or an empty string if not set.
281      * @since 3.0.0
282      */
283     public String getOsFamily(int index) {
284         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.OS_FAMLY, getOsFamily());
285     }
286 
287     public Collection<InvokerToolchain> getToolchains() {
288         return getToolchains(Pattern.compile("invoker\\.toolchain\\.([^.]+)\\.(.+)"));
289     }
290 
291     public Collection<InvokerToolchain> getToolchains(int index) {
292         return getToolchains(Pattern.compile("selector\\." + index + "\\.invoker\\.toolchain\\.([^.]+)\\.(.+)"));
293     }
294 
295     private Collection<InvokerToolchain> getToolchains(Pattern p) {
296         Map<String, InvokerToolchain> toolchains = new HashMap<>();
297         for (Map.Entry<Object, Object> entry : this.properties.entrySet()) {
298             Matcher m = p.matcher(entry.getKey().toString());
299             if (m.matches()) {
300                 String type = m.group(1);
301                 String providesKey = m.group(2);
302                 String providesValue = entry.getValue().toString();
303 
304                 InvokerToolchain tc = toolchains.get(type);
305                 if (tc == null) {
306                     tc = new InvokerToolchain(type);
307                     toolchains.put(type, tc);
308                 }
309                 tc.addProvides(providesKey, providesValue);
310             }
311         }
312         return toolchains.values();
313     }
314 
315     /**
316      * Extract environment variable from properties for given index.
317      * Every environment variable without index is also returned.
318      *
319      * @param index index to lookup
320      * @return map of environment name and value
321      */
322     private Map<String, String> getEnvironmentVariables(int index) {
323 
324         Map<String, String> envItems = new HashMap<>();
325 
326         for (Map.Entry<Object, Object> entry : properties.entrySet()) {
327             Matcher matcher =
328                     ENVIRONMENT_VARIABLES_PATTERN.matcher(entry.getKey().toString());
329             if (matcher.matches()) {
330 
331                 if (String.valueOf(index).equals(matcher.group(3))) {
332                     // variables with index has higher priority, so override
333                     envItems.put(matcher.group(1), entry.getValue().toString());
334                 } else if (matcher.group(3) == null) {
335                     // variables without index has lower priority, so check if exist
336                     if (!envItems.containsKey(matcher.group(1))) {
337                         envItems.put(matcher.group(1), entry.getValue().toString());
338                     }
339                 }
340             }
341         }
342         return envItems;
343     }
344 
345     /**
346      * Determines whether these invoker properties contain a build definition for the specified invocation index.
347      *
348      * @param index The one-based index of the invocation to check for, must not be negative.
349      * @return <code>true</code> if the invocation with the specified index is defined, <code>false</code> otherwise.
350      */
351     public boolean isInvocationDefined(int index) {
352         return Arrays.stream(InvocationProperty.values())
353                 .map(InvocationProperty::toString)
354                 .map(v -> properties.getProperty(v + '.' + index))
355                 .anyMatch(Objects::nonNull);
356     }
357 
358     /**
359      * Determines whether these invoker properties contain a build definition for the specified selector index.
360      *
361      * @param index the index
362      * @return <code>true</code> if the selector with the specified index is defined, <code>false</code> otherwise.
363      * @since 3.0.0
364      */
365     public boolean isSelectorDefined(int index) {
366         return Arrays.stream(SelectorProperty.values())
367                 .map(v -> v.suffix)
368                 .map(v -> properties.getProperty(SELECTOR_PREFIX + index + v))
369                 .anyMatch(Objects::nonNull);
370     }
371 
372     private <T> void setIfNotNull(Consumer<T> consumer, T value) {
373         if (value != null) {
374             consumer.accept(value);
375         }
376     }
377 
378     /**
379      * Configures the specified invocation request from these invoker properties. Settings not present in the invoker
380      * properties will be left unchanged in the invocation request.
381      *
382      * @param request The invocation request to configure, must not be <code>null</code>.
383      * @param index The one-based index of the invocation to configure, must not be negative.
384      */
385     public void configureInvocation(InvocationRequest request, int index) {
386         get(InvocationProperty.PROJECT, index).ifPresent(project -> {
387             File file = new File(request.getBaseDirectory(), project);
388             if (file.isFile()) {
389                 request.setBaseDirectory(file.getParentFile());
390                 request.setPomFile(file);
391             } else {
392                 request.setBaseDirectory(file);
393                 request.setPomFile(null);
394             }
395         });
396 
397         setIfNotNull(
398                 request::setGoals,
399                 get(InvocationProperty.GOALS, index)
400                         .map(s -> StringUtils.split(s, ", \t\n\r\f"))
401                         .map(Arrays::asList)
402                         .filter(l -> !l.isEmpty())
403                         .orElse(defaultGoals));
404 
405         setIfNotNull(
406                 request::setProfiles,
407                 get(InvocationProperty.PROFILES, index)
408                         .map(s -> StringUtils.split(s, ", \t\n\r\f"))
409                         .map(Arrays::asList)
410                         .filter(l -> !l.isEmpty())
411                         .orElse(defaultProfiles));
412 
413         setIfNotNull(
414                 request::setMavenExecutable,
415                 get(InvocationProperty.MAVEN_EXECUTABLE, index).map(File::new).orElse(defaultMavenExecutable));
416 
417         setIfNotNull(
418                 request::setMavenOpts, get(InvocationProperty.MAVEN_OPTS, index).orElse(defaultMavenOpts));
419 
420         get(InvocationProperty.FAILURE_BEHAVIOR, index)
421                 .map(InvocationRequest.ReactorFailureBehavior::valueOfByLongOption)
422                 .ifPresent(request::setReactorFailureBehavior);
423 
424         get(InvocationProperty.NON_RECURSIVE, index)
425                 .map(Boolean::parseBoolean)
426                 .map(b -> !b)
427                 .ifPresent(request::setRecursive);
428 
429         get(InvocationProperty.OFFLINE, index).map(Boolean::parseBoolean).ifPresent(request::setOffline);
430 
431         setIfNotNull(
432                 request::setDebug,
433                 get(InvocationProperty.DEBUG, index).map(Boolean::parseBoolean).orElse(defaultDebug));
434 
435         setIfNotNull(
436                 request::setQuiet,
437                 get(InvocationProperty.QUIET, index).map(Boolean::parseBoolean).orElse(defaultQuiet));
438 
439         setIfNotNull(
440                 request::setTimeoutInSeconds,
441                 get(InvocationProperty.TIMEOUT_IN_SECONDS, index)
442                         .map(Integer::parseInt)
443                         .orElse(defaultTimeoutInSeconds));
444 
445         setIfNotNull(
446                 request::setUpdateSnapshots,
447                 get(InvocationProperty.UPDATE_SNAPSHOTS, index)
448                         .map(Boolean::parseBoolean)
449                         .orElse(defaultUpdateSnapshots));
450 
451         Optional.ofNullable(defaultEnvironmentVariables).ifPresent(evn -> evn.forEach(request::addShellEnvironment));
452 
453         getEnvironmentVariables(index).forEach(request::addShellEnvironment);
454     }
455 
456     /**
457      * Checks whether the specified exit code matches the one expected for the given invocation.
458      *
459      * @param exitCode The exit code of the Maven invocation to check.
460      * @param index The index of the invocation for which to check the exit code, must not be negative.
461      * @return <code>true</code> if the exit code is zero and a success was expected or if the exit code is non-zero and
462      *         a failue was expected, <code>false</code> otherwise.
463      */
464     public boolean isExpectedResult(int exitCode, int index) {
465         boolean nonZeroExit = "failure"
466                 .equalsIgnoreCase(get(InvocationProperty.BUILD_RESULT, index).orElse(null));
467         return (exitCode != 0) == nonZeroExit;
468     }
469 
470     /**
471      * Gets the path to the properties file used to set the system properties for the specified invocation.
472      *
473      * @param index The index of the invocation, must not be negative.
474      * @return The path to the properties file or <code>null</code> if not set.
475      */
476     public String getSystemPropertiesFile(int index) {
477         return get(InvocationProperty.SYSTEM_PROPERTIES_FILE, index).orElse(null);
478     }
479 
480     /**
481      * Gets the settings file used for the specified invocation.
482      *
483      * @param index The index of the invocation, must not be negative.
484      * @return the value for the settings file or <code>null</code> if not set.
485      */
486     public String getSettingsFile(int index) {
487         return get(InvocationProperty.SETTINGS_FILE, index).orElse(null);
488     }
489 
490     /**
491      * Gets a value from the invoker properties. The invoker properties are intended to describe the invocation settings
492      * for multiple builds of the same project. For this reason, the properties are indexed. First, a property named
493      * <code>key.index</code> will be queried. If this property does not exist, the value of the property named
494      * <code>key</code> will finally be returned.
495      *
496      * @param key The (base) key for the invoker property to lookup, must not be <code>null</code>.
497      * @param index The index of the invocation for which to retrieve the value, must not be negative.
498      * @return The value for the requested invoker property or <code>null</code> if not defined.
499      */
500     Optional<String> get(String key, int index) {
501         if (index < 0) {
502             throw new IllegalArgumentException("invalid invocation index: " + index);
503         }
504 
505         // lookup in properties
506         String value = Optional.ofNullable(properties.getProperty(key + '.' + index))
507                 .orElseGet(() -> properties.getProperty(key));
508 
509         return Optional.ofNullable(value).map(String::trim).filter(s -> !s.isEmpty());
510     }
511 
512     private Optional<String> get(InvocationProperty prop, int index) {
513         return get(prop.toString(), index);
514     }
515 }