Coverage Report - org.apache.maven.shared.utils.cli.CommandLineUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
CommandLineUtils
44%
40/90
56%
29/51
4.176
CommandLineUtils$1
0%
0/29
0%
0/16
4.176
CommandLineUtils$ProcessHook
0%
0/7
N/A
4.176
CommandLineUtils$StringStreamConsumer
0%
0/6
N/A
4.176
 
 1  
 package org.apache.maven.shared.utils.cli;
 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.IOException;
 23  
 import java.io.InputStream;
 24  
 import java.lang.reflect.InvocationTargetException;
 25  
 import java.util.ArrayList;
 26  
 import java.util.List;
 27  
 import java.util.Locale;
 28  
 import java.util.Map;
 29  
 import java.util.Properties;
 30  
 import java.util.StringTokenizer;
 31  
 import org.apache.maven.shared.utils.Os;
 32  
 import org.apache.maven.shared.utils.StringUtils;
 33  
 
 34  
 /**
 35  
  * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l </a>
 36  
  * @version $Id: CommandLineUtils.java 1406293 2012-11-06 20:04:00Z krosenvold $
 37  
  */
 38  0
 public abstract class CommandLineUtils
 39  
 {
 40  
 
 41  
 
 42  0
     public static class StringStreamConsumer
 43  
         implements StreamConsumer
 44  
     {
 45  0
         private final StringBuffer string = new StringBuffer();
 46  
 
 47  0
         private static final String LS = System.getProperty( "line.separator" );
 48  
 
 49  
         public void consumeLine( String line )
 50  
         {
 51  0
             string.append( line ).append( LS );
 52  0
         }
 53  
 
 54  
         public String getOutput()
 55  
         {
 56  0
             return string.toString();
 57  
         }
 58  
     }
 59  
 
 60  0
     private static class ProcessHook
 61  
         extends Thread
 62  
     {
 63  
         private final Process process;
 64  
 
 65  
         private ProcessHook( Process process )
 66  
         {
 67  0
             super( "CommandlineUtils process shutdown hook" );
 68  0
             this.process = process;
 69  0
             this.setContextClassLoader( null );
 70  0
         }
 71  
 
 72  
         public void run()
 73  
         {
 74  0
             process.destroy();
 75  0
         }
 76  
     }
 77  
 
 78  
 
 79  
     public static int executeCommandLine( Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr )
 80  
         throws CommandLineException
 81  
     {
 82  0
         return executeCommandLine( cl, null, systemOut, systemErr, 0 );
 83  
     }
 84  
 
 85  
     public static int executeCommandLine( Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr,
 86  
                                           int timeoutInSeconds )
 87  
         throws CommandLineException
 88  
     {
 89  0
         return executeCommandLine( cl, null, systemOut, systemErr, timeoutInSeconds );
 90  
     }
 91  
 
 92  
     public static int executeCommandLine( Commandline cl, InputStream systemIn, StreamConsumer systemOut,
 93  
                                           StreamConsumer systemErr )
 94  
         throws CommandLineException
 95  
     {
 96  0
         return executeCommandLine( cl, systemIn, systemOut, systemErr, 0 );
 97  
     }
 98  
 
 99  
     /**
 100  
      * @param cl               The command line to execute
 101  
      * @param systemIn         The input to read from, must be thread safe
 102  
      * @param systemOut        A consumer that receives output, must be thread safe
 103  
      * @param systemErr        A consumer that receives system error stream output, must be thread safe
 104  
      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
 105  
      * @return A return value, see {@link Process#exitValue()}
 106  
      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
 107  
      * @noinspection ThrowableResultOfMethodCallIgnored
 108  
      */
 109  
     public static int executeCommandLine( Commandline cl, InputStream systemIn, StreamConsumer systemOut,
 110  
                                           StreamConsumer systemErr, int timeoutInSeconds )
 111  
         throws CommandLineException
 112  
     {
 113  0
         final CommandLineCallable future =
 114  
             executeCommandLineAsCallable( cl, systemIn, systemOut, systemErr, timeoutInSeconds );
 115  0
         return future.call();
 116  
     }
 117  
 
 118  
     /**
 119  
      * Immediately forks a process, returns a callable that will block until process is complete.
 120  
      *
 121  
      * @param cl               The command line to execute
 122  
      * @param systemIn         The input to read from, must be thread safe
 123  
      * @param systemOut        A consumer that receives output, must be thread safe
 124  
      * @param systemErr        A consumer that receives system error stream output, must be thread safe
 125  
      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
 126  
      * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call" must be called on
 127  
      *         this to be sure the forked process has terminated, no guarantees is made about
 128  
      *         any internal state before after the completion of the call statements
 129  
      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
 130  
      * @noinspection ThrowableResultOfMethodCallIgnored
 131  
      */
 132  
     private static CommandLineCallable executeCommandLineAsCallable( final Commandline cl, final InputStream systemIn,
 133  
                                                                     final StreamConsumer systemOut,
 134  
                                                                     final StreamConsumer systemErr,
 135  
                                                                     final int timeoutInSeconds )
 136  
         throws CommandLineException
 137  
     {
 138  0
         if ( cl == null )
 139  
         {
 140  0
             throw new IllegalArgumentException( "cl cannot be null." );
 141  
         }
 142  
 
 143  0
         final Process p = cl.execute();
 144  
 
 145  0
         final StreamFeeder inputFeeder = systemIn != null ? new StreamFeeder( systemIn, p.getOutputStream() ) : null;
 146  
 
 147  0
         final StreamPumper outputPumper = new StreamPumper( p.getInputStream(), systemOut );
 148  
 
 149  0
         final StreamPumper errorPumper = new StreamPumper( p.getErrorStream(), systemErr );
 150  
 
 151  0
         if ( inputFeeder != null )
 152  
         {
 153  0
             inputFeeder.start();
 154  
         }
 155  
 
 156  0
         outputPumper.start();
 157  
 
 158  0
         errorPumper.start();
 159  
 
 160  0
         final ProcessHook processHook = new ProcessHook( p );
 161  
 
 162  0
         ShutdownHookUtils.addShutDownHook( processHook );
 163  
 
 164  0
         return new CommandLineCallable()
 165  
         {
 166  0
             public Integer call()
 167  
                 throws CommandLineException
 168  
             {
 169  
                 try
 170  
                 {
 171  
                     int returnValue;
 172  0
                     if ( timeoutInSeconds <= 0 )
 173  
                     {
 174  0
                         returnValue = p.waitFor();
 175  
                     }
 176  
                     else
 177  
                     {
 178  0
                         long now = System.currentTimeMillis();
 179  0
                         long timeoutInMillis = 1000L * timeoutInSeconds;
 180  0
                         long finish = now + timeoutInMillis;
 181  0
                         while ( isAlive( p ) && ( System.currentTimeMillis() < finish ) )
 182  
                         {
 183  0
                             Thread.sleep( 10 );
 184  
                         }
 185  0
                         if ( isAlive( p ) )
 186  
                         {
 187  0
                             throw new InterruptedException(
 188  
                                 "Process timeout out after " + timeoutInSeconds + " seconds" );
 189  
                         }
 190  
 
 191  0
                         returnValue = p.exitValue();
 192  
                     }
 193  
 
 194  0
                     waitForAllPumpers( inputFeeder, outputPumper, errorPumper );
 195  
 
 196  0
                     if ( outputPumper.getException() != null )
 197  
                     {
 198  0
                         throw new CommandLineException( "Error inside systemOut parser", outputPumper.getException() );
 199  
                     }
 200  
 
 201  0
                     if ( errorPumper.getException() != null )
 202  
                     {
 203  0
                         throw new CommandLineException( "Error inside systemErr parser", errorPumper.getException() );
 204  
                     }
 205  
 
 206  0
                     return returnValue;
 207  
                 }
 208  0
                 catch ( InterruptedException ex )
 209  
                 {
 210  0
                     if ( inputFeeder != null )
 211  
                     {
 212  0
                         inputFeeder.disable();
 213  
                     }
 214  
 
 215  0
                     outputPumper.disable();
 216  0
                     errorPumper.disable();
 217  0
                     throw new CommandLineTimeOutException( "Error while executing external command, process killed.",
 218  
                                                            ex );
 219  
                 }
 220  
                 finally
 221  
                 {
 222  0
                     ShutdownHookUtils.removeShutdownHook( processHook );
 223  
 
 224  0
                     processHook.run();
 225  
 
 226  0
                     if ( inputFeeder != null )
 227  
                     {
 228  0
                         inputFeeder.close();
 229  
                     }
 230  
 
 231  0
                     outputPumper.close();
 232  
 
 233  0
                     errorPumper.close();
 234  
                 }
 235  
             }
 236  
         };
 237  
     }
 238  
 
 239  
     private static void waitForAllPumpers( StreamFeeder inputFeeder, StreamPumper outputPumper,
 240  
                                            StreamPumper errorPumper )
 241  
         throws InterruptedException
 242  
     {
 243  0
         if ( inputFeeder != null )
 244  
         {
 245  0
             inputFeeder.waitUntilDone();
 246  
         }
 247  
 
 248  0
         outputPumper.waitUntilDone();
 249  0
         errorPumper.waitUntilDone();
 250  0
     }
 251  
 
 252  
     /**
 253  
      * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
 254  
      * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
 255  
      * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
 256  
      * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
 257  
      *
 258  
      * @return The shell environment variables, can be empty but never <code>null</code>.
 259  
      * @throws IOException If the environment variables could not be queried from the shell.
 260  
      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
 261  
      *      <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
 262  
      */
 263  
     public static Properties getSystemEnvVars()
 264  
         throws IOException
 265  
     {
 266  0
         return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
 267  
     }
 268  
 
 269  
     /**
 270  
      * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar
 271  
      * keys will all be upper-case.
 272  
      *
 273  
      * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
 274  
      * @return Properties object of (possibly modified) envar keys mapped to their values.
 275  
      * @throws IOException .
 276  
      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
 277  
      *      <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
 278  
      */
 279  
     public static Properties getSystemEnvVars( boolean caseSensitive )
 280  
         throws IOException
 281  
     {
 282  
 
 283  
         // check if it's 1.5+ run
 284  
 
 285  
         try
 286  
         {
 287  1
             Map<String, String> envs = System.getenv();
 288  1
             return ensureCaseSensitivity( envs, caseSensitive );
 289  
         }
 290  0
         catch ( IllegalAccessException e )
 291  
         {
 292  0
             throw new IOException( e.getMessage() );
 293  
         }
 294  0
         catch ( IllegalArgumentException e )
 295  
         {
 296  0
             throw new IOException( e.getMessage() );
 297  
         }
 298  0
         catch ( InvocationTargetException e )
 299  
         {
 300  0
             throw new IOException( e.getMessage() );
 301  
         }
 302  
     }
 303  
 
 304  
     private static boolean isAlive( Process p )
 305  
     {
 306  0
         if ( p == null )
 307  
         {
 308  0
             return false;
 309  
         }
 310  
 
 311  
         try
 312  
         {
 313  0
             p.exitValue();
 314  0
             return false;
 315  
         }
 316  0
         catch ( IllegalThreadStateException e )
 317  
         {
 318  0
             return true;
 319  
         }
 320  
     }
 321  
 
 322  
     public static String[] translateCommandline( String toProcess )
 323  
         throws Exception
 324  
     {
 325  8
         if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
 326  
         {
 327  2
             return new String[0];
 328  
         }
 329  
 
 330  
         // parse with a simple finite state machine
 331  
 
 332  6
         final int normal = 0;
 333  6
         final int inQuote = 1;
 334  6
         final int inDoubleQuote = 2;
 335  6
         int state = normal;
 336  6
         StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
 337  6
         List<String> tokens = new ArrayList<String>();
 338  6
         StringBuilder current = new StringBuilder();
 339  
 
 340  60
         while ( tok.hasMoreTokens() )
 341  
         {
 342  54
             String nextTok = tok.nextToken();
 343  54
             switch ( state )
 344  
             {
 345  
                 case inQuote:
 346  10
                     if ( "\'".equals( nextTok ) )
 347  
                     {
 348  2
                         state = normal;
 349  
                     }
 350  
                     else
 351  
                     {
 352  8
                         current.append( nextTok );
 353  
                     }
 354  8
                     break;
 355  
                 case inDoubleQuote:
 356  10
                     if ( "\"".equals( nextTok ) )
 357  
                     {
 358  2
                         state = normal;
 359  
                     }
 360  
                     else
 361  
                     {
 362  8
                         current.append( nextTok );
 363  
                     }
 364  8
                     break;
 365  
                 default:
 366  34
                     if ( "\'".equals( nextTok ) )
 367  
                     {
 368  2
                         state = inQuote;
 369  
                     }
 370  32
                     else if ( "\"".equals( nextTok ) )
 371  
                     {
 372  2
                         state = inDoubleQuote;
 373  
                     }
 374  30
                     else if ( " ".equals( nextTok ) )
 375  
                     {
 376  18
                         if ( current.length() != 0 )
 377  
                         {
 378  11
                             tokens.add( current.toString() );
 379  11
                             current.setLength( 0 );
 380  
                         }
 381  
                     }
 382  
                     else
 383  
                     {
 384  12
                         current.append( nextTok );
 385  
                     }
 386  
                     break;
 387  
             }
 388  54
         }
 389  
 
 390  6
         if ( current.length() != 0 )
 391  
         {
 392  5
             tokens.add( current.toString() );
 393  
         }
 394  
 
 395  6
         if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
 396  
         {
 397  0
             throw new CommandLineException( "unbalanced quotes in " + toProcess );
 398  
         }
 399  
 
 400  6
         return tokens.toArray( new String[tokens.size()] );
 401  
     }
 402  
 
 403  
     public static String toString( String... line )
 404  
     {
 405  
         // empty path return empty string
 406  0
         if ( ( line == null ) || ( line.length == 0 ) )
 407  
         {
 408  0
             return "";
 409  
         }
 410  
 
 411  
         // path containing one or more elements
 412  0
         final StringBuilder result = new StringBuilder();
 413  0
         for ( int i = 0; i < line.length; i++ )
 414  
         {
 415  0
             if ( i > 0 )
 416  
             {
 417  0
                 result.append( ' ' );
 418  
             }
 419  
             try
 420  
             {
 421  0
                 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
 422  
             }
 423  0
             catch ( Exception e )
 424  
             {
 425  0
                 System.err.println( "Error quoting argument: " + e.getMessage() );
 426  0
             }
 427  
         }
 428  0
         return result.toString();
 429  
     }
 430  
 
 431  
     static Properties ensureCaseSensitivity( Map<String, String> envs, boolean preserveKeyCase )
 432  
         throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
 433  
     {
 434  3
         Properties envVars = new Properties();
 435  3
         for ( Map.Entry<String, String> entry : envs.entrySet() )
 436  
         {
 437  30
             envVars.put( !preserveKeyCase ? entry.getKey().toUpperCase( Locale.ENGLISH ) : entry.getKey(),
 438  
                          entry.getValue() );
 439  
         }
 440  3
         return envVars;
 441  
     }
 442  
 }