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