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  import org.codehaus.plexus.util.StringUtils;
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(File defaultMavenExecutable) {
157         this.defaultMavenExecutable = defaultMavenExecutable;
158     }
159 
160     /**
161      * Default value for mavenOpts
162      * @param defaultMavenOpts a default value
163      */
164     public void setDefaultMavenOpts(String defaultMavenOpts) {
165         this.defaultMavenOpts = defaultMavenOpts;
166     }
167 
168     /**
169      * Default value for timeoutInSeconds
170      * @param defaultTimeoutInSeconds a default value
171      */
172     public void setDefaultTimeoutInSeconds(int defaultTimeoutInSeconds) {
173         this.defaultTimeoutInSeconds = defaultTimeoutInSeconds;
174     }
175 
176     /**
177      * Default value for environmentVariables
178      * @param defaultEnvironmentVariables a default value
179      */
180     public void setDefaultEnvironmentVariables(Map<String, String> defaultEnvironmentVariables) {
181         this.defaultEnvironmentVariables = defaultEnvironmentVariables;
182     }
183 
184     /**
185      * Default value for updateSnapshots
186      * @param defaultUpdateSnapshots a default value
187      */
188     public void setDefaultUpdateSnapshots(boolean defaultUpdateSnapshots) {
189         this.defaultUpdateSnapshots = defaultUpdateSnapshots;
190     }
191 
192     /**
193      * Gets the invoker properties being wrapped.
194      *
195      * @return The invoker properties being wrapped, never <code>null</code>.
196      */
197     public Properties getProperties() {
198         return this.properties;
199     }
200 
201     /**
202      * Gets the name of the corresponding build job.
203      *
204      * @return The name of the build job or an empty string if not set.
205      */
206     public String getJobName() {
207         return this.properties.getProperty("invoker.name", "");
208     }
209 
210     /**
211      * Gets the description of the corresponding build job.
212      *
213      * @return The description of the build job or an empty string if not set.
214      */
215     public String getJobDescription() {
216         return this.properties.getProperty("invoker.description", "");
217     }
218 
219     /**
220      * Get the corresponding ordinal value
221      *
222      * @return The ordinal value
223      */
224     public int getOrdinal() {
225         return Integer.parseInt(this.properties.getProperty("invoker.ordinal", "0"));
226     }
227 
228     /**
229      * Gets the specification of JRE versions on which this build job should be run.
230      *
231      * @return The specification of JRE versions or an empty string if not set.
232      */
233     public String getJreVersion() {
234         return this.properties.getProperty("invoker.java.version", "");
235     }
236 
237     /**
238      * Gets the specification of JRE versions on which this build job should be run.
239      *
240      * @return The specification of JRE versions or an empty string if not set.
241      */
242     public String getJreVersion(int index) {
243         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.JAVA_VERSION, getJreVersion());
244     }
245 
246     /**
247      * Gets the specification of Maven versions on which this build job should be run.
248      *
249      * @return The specification of Maven versions on which this build job should be run.
250      * @since 1.5
251      */
252     public String getMavenVersion() {
253         return this.properties.getProperty("invoker.maven.version", "");
254     }
255 
256     /**
257      * @param index the selector index
258      * @return The specification of Maven versions on which this build job should be run.
259      * @since 3.0.0
260      */
261     public String getMavenVersion(int index) {
262         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.MAVEN_VERSION, getMavenVersion());
263     }
264 
265     /**
266      * Gets the specification of OS families on which this build job should be run.
267      *
268      * @return The specification of OS families or an empty string if not set.
269      */
270     public String getOsFamily() {
271         return this.properties.getProperty("invoker.os.family", "");
272     }
273 
274     /**
275      * Gets the specification of OS families on which this build job should be run.
276      *
277      * @param index the selector index
278      * @return The specification of OS families or an empty string if not set.
279      * @since 3.0.0
280      */
281     public String getOsFamily(int index) {
282         return this.properties.getProperty(SELECTOR_PREFIX + index + SelectorProperty.OS_FAMLY, getOsFamily());
283     }
284 
285     public Collection<InvokerToolchain> getToolchains() {
286         return getToolchains(Pattern.compile("invoker\\.toolchain\\.([^.]+)\\.(.+)"));
287     }
288 
289     public Collection<InvokerToolchain> getToolchains(int index) {
290         return getToolchains(Pattern.compile("selector\\." + index + "\\.invoker\\.toolchain\\.([^.]+)\\.(.+)"));
291     }
292 
293     private Collection<InvokerToolchain> getToolchains(Pattern p) {
294         Map<String, InvokerToolchain> toolchains = new HashMap<>();
295         for (Map.Entry<Object, Object> entry : this.properties.entrySet()) {
296             Matcher m = p.matcher(entry.getKey().toString());
297             if (m.matches()) {
298                 String type = m.group(1);
299                 String providesKey = m.group(2);
300                 String providesValue = entry.getValue().toString();
301 
302                 InvokerToolchain tc = toolchains.get(type);
303                 if (tc == null) {
304                     tc = new InvokerToolchain(type);
305                     toolchains.put(type, tc);
306                 }
307                 tc.addProvides(providesKey, providesValue);
308             }
309         }
310         return toolchains.values();
311     }
312 
313     /**
314      * Extract environment variable from properties for given index.
315      * Every environment variable without index is also returned.
316      *
317      * @param index index to lookup
318      * @return map of environment name and value
319      */
320     private Map<String, String> getEnvironmentVariables(int index) {
321 
322         Map<String, String> envItems = new HashMap<>();
323 
324         for (Map.Entry<Object, Object> entry : properties.entrySet()) {
325             Matcher matcher =
326                     ENVIRONMENT_VARIABLES_PATTERN.matcher(entry.getKey().toString());
327             if (matcher.matches()) {
328 
329                 if (String.valueOf(index).equals(matcher.group(3))) {
330                     // variables with index has higher priority, so override
331                     envItems.put(matcher.group(1), entry.getValue().toString());
332                 } else if (matcher.group(3) == null) {
333                     // variables without index has lower priority, so check if exist
334                     if (!envItems.containsKey(matcher.group(1))) {
335                         envItems.put(matcher.group(1), entry.getValue().toString());
336                     }
337                 }
338             }
339         }
340         return envItems;
341     }
342 
343     /**
344      * Determines whether these invoker properties contain a build definition for the specified invocation index.
345      *
346      * @param index The one-based index of the invocation to check for, must not be negative.
347      * @return <code>true</code> if the invocation with the specified index is defined, <code>false</code> otherwise.
348      */
349     public boolean isInvocationDefined(int index) {
350         return Arrays.stream(InvocationProperty.values())
351                 .map(InvocationProperty::toString)
352                 .map(v -> properties.getProperty(v + '.' + index))
353                 .anyMatch(Objects::nonNull);
354     }
355 
356     /**
357      * Determines whether these invoker properties contain a build definition for the specified selector index.
358      *
359      * @param index the index
360      * @return <code>true</code> if the selector with the specified index is defined, <code>false</code> otherwise.
361      * @since 3.0.0
362      */
363     public boolean isSelectorDefined(int index) {
364         return Arrays.stream(SelectorProperty.values())
365                 .map(v -> v.suffix)
366                 .map(v -> properties.getProperty(SELECTOR_PREFIX + index + v))
367                 .anyMatch(Objects::nonNull);
368     }
369 
370     private <T> void setIfNotNull(Consumer<T> consumer, T value) {
371         if (value != null) {
372             consumer.accept(value);
373         }
374     }
375 
376     /**
377      * Configures the specified invocation request from these invoker properties. Settings not present in the invoker
378      * properties will be left unchanged in the invocation request.
379      *
380      * @param request The invocation request to configure, must not be <code>null</code>.
381      * @param index The one-based index of the invocation to configure, must not be negative.
382      */
383     public void configureInvocation(InvocationRequest request, int index) {
384         get(InvocationProperty.PROJECT, index).ifPresent(project -> {
385             File file = new File(request.getBaseDirectory(), project);
386             if (file.isFile()) {
387                 request.setBaseDirectory(file.getParentFile());
388                 request.setPomFile(file);
389             } else {
390                 request.setBaseDirectory(file);
391                 request.setPomFile(null);
392             }
393         });
394 
395         setIfNotNull(
396                 request::setGoals,
397                 get(InvocationProperty.GOALS, index)
398                         .map(s -> StringUtils.split(s, ", \t\n\r\f"))
399                         .map(Arrays::asList)
400                         .filter(l -> !l.isEmpty())
401                         .orElse(defaultGoals));
402 
403         setIfNotNull(
404                 request::setProfiles,
405                 get(InvocationProperty.PROFILES, index)
406                         .map(s -> StringUtils.split(s, ", \t\n\r\f"))
407                         .map(Arrays::asList)
408                         .filter(l -> !l.isEmpty())
409                         .orElse(defaultProfiles));
410 
411         setIfNotNull(
412                 request::setMavenExecutable,
413                 get(InvocationProperty.MAVEN_EXECUTABLE, index).map(File::new).orElse(defaultMavenExecutable));
414 
415         setIfNotNull(
416                 request::setMavenOpts, get(InvocationProperty.MAVEN_OPTS, index).orElse(defaultMavenOpts));
417 
418         get(InvocationProperty.FAILURE_BEHAVIOR, index)
419                 .map(InvocationRequest.ReactorFailureBehavior::valueOfByLongOption)
420                 .ifPresent(request::setReactorFailureBehavior);
421 
422         get(InvocationProperty.NON_RECURSIVE, index)
423                 .map(Boolean::parseBoolean)
424                 .map(b -> !b)
425                 .ifPresent(request::setRecursive);
426 
427         get(InvocationProperty.OFFLINE, index).map(Boolean::parseBoolean).ifPresent(request::setOffline);
428 
429         setIfNotNull(
430                 request::setDebug,
431                 get(InvocationProperty.DEBUG, index).map(Boolean::parseBoolean).orElse(defaultDebug));
432 
433         setIfNotNull(
434                 request::setQuiet,
435                 get(InvocationProperty.QUIET, index).map(Boolean::parseBoolean).orElse(defaultQuiet));
436 
437         setIfNotNull(
438                 request::setTimeoutInSeconds,
439                 get(InvocationProperty.TIMEOUT_IN_SECONDS, index)
440                         .map(Integer::parseInt)
441                         .orElse(defaultTimeoutInSeconds));
442 
443         setIfNotNull(
444                 request::setUpdateSnapshots,
445                 get(InvocationProperty.UPDATE_SNAPSHOTS, index)
446                         .map(Boolean::parseBoolean)
447                         .orElse(defaultUpdateSnapshots));
448 
449         Optional.ofNullable(defaultEnvironmentVariables).ifPresent(evn -> evn.forEach(request::addShellEnvironment));
450 
451         getEnvironmentVariables(index).forEach(request::addShellEnvironment);
452     }
453 
454     /**
455      * Checks whether the specified exit code matches the one expected for the given invocation.
456      *
457      * @param exitCode The exit code of the Maven invocation to check.
458      * @param index The index of the invocation for which to check the exit code, must not be negative.
459      * @return <code>true</code> if the exit code is zero and a success was expected or if the exit code is non-zero and
460      *         a failue was expected, <code>false</code> otherwise.
461      */
462     public boolean isExpectedResult(int exitCode, int index) {
463         boolean nonZeroExit = "failure"
464                 .equalsIgnoreCase(get(InvocationProperty.BUILD_RESULT, index).orElse(null));
465         return (exitCode != 0) == nonZeroExit;
466     }
467 
468     /**
469      * Gets the path to the properties file used to set the system properties for the specified invocation.
470      *
471      * @param index The index of the invocation, must not be negative.
472      * @return The path to the properties file or <code>null</code> if not set.
473      */
474     public String getSystemPropertiesFile(int index) {
475         return get(InvocationProperty.SYSTEM_PROPERTIES_FILE, index).orElse(null);
476     }
477 
478     /**
479      * Gets the settings file used for the specified invocation.
480      *
481      * @param index The index of the invocation, must not be negative.
482      * @return the value for the settings file or <code>null</code> if not set.
483      */
484     public String getSettingsFile(int index) {
485         return get(InvocationProperty.SETTINGS_FILE, index).orElse(null);
486     }
487 
488     /**
489      * Gets a value from the invoker properties. The invoker properties are intended to describe the invocation settings
490      * for multiple builds of the same project. For this reason, the properties are indexed. First, a property named
491      * <code>key.index</code> will be queried. If this property does not exist, the value of the property named
492      * <code>key</code> will finally be returned.
493      *
494      * @param key The (base) key for the invoker property to lookup, must not be <code>null</code>.
495      * @param index The index of the invocation for which to retrieve the value, must not be negative.
496      * @return The value for the requested invoker property or <code>null</code> if not defined.
497      */
498     Optional<String> get(String key, int index) {
499         if (index < 0) {
500             throw new IllegalArgumentException("invalid invocation index: " + index);
501         }
502 
503         // lookup in properties
504         String value = Optional.ofNullable(properties.getProperty(key + '.' + index))
505                 .orElseGet(() -> properties.getProperty(key));
506 
507         return Optional.ofNullable(value).map(String::trim).filter(s -> !s.isEmpty());
508     }
509 
510     private Optional<String> get(InvocationProperty prop, int index) {
511         return get(prop.toString(), index);
512     }
513 }