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