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