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