Coverage Report - org.apache.maven.shared.scriptinterpreter.ScriptRunner
 
Classes in this File Line Coverage Branch Coverage Complexity
ScriptRunner
60%
51/85
34%
16/46
4
 
 1  
 package org.apache.maven.shared.scriptinterpreter;
 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 org.apache.maven.plugin.logging.Log;
 23  
 import org.codehaus.plexus.util.FileUtils;
 24  
 import org.codehaus.plexus.util.StringUtils;
 25  
 
 26  
 import java.io.File;
 27  
 import java.io.IOException;
 28  
 import java.io.PrintStream;
 29  
 import java.util.ArrayList;
 30  
 import java.util.HashMap;
 31  
 import java.util.LinkedHashMap;
 32  
 import java.util.List;
 33  
 import java.util.Locale;
 34  
 import java.util.Map;
 35  
 
 36  
 /**
 37  
  * Runs pre-/post-build hook scripts.
 38  
  *
 39  
  * @author Benjamin Bentmann
 40  
  * @version $Id: ScriptRunner.java 1361825 2012-07-15 22:21:49Z hboutemy $
 41  
  */
 42  
 public class ScriptRunner
 43  
 {
 44  
 
 45  
     /**
 46  
      * The mojo logger to print diagnostic to, never <code>null</code>.
 47  
      */
 48  
     private Log log;
 49  
 
 50  
     /**
 51  
      * The supported script interpreters, indexed by the lower-case file extension of their associated script files,
 52  
      * never <code>null</code>.
 53  
      */
 54  
     private Map<String, ScriptInterpreter> scriptInterpreters;
 55  
 
 56  
     /**
 57  
      * The common set of global variables to pass into the script interpreter, never <code>null</code>.
 58  
      */
 59  
     private Map<String, Object> globalVariables;
 60  
 
 61  
     /**
 62  
      * The additional class path for the script interpreter, never <code>null</code>.
 63  
      */
 64  
     private List<String> classPath;
 65  
 
 66  
     /**
 67  
      * The file encoding of the hook scripts or <code>null</code> to use platform encoding.
 68  
      */
 69  
     private String encoding;
 70  
 
 71  
     /**
 72  
      * Creates a new script runner.
 73  
      *
 74  
      * @param log The mojo logger to print diagnostic to, must not be <code>null</code>.
 75  
      */
 76  
     public ScriptRunner( Log log )
 77  8
     {
 78  8
         if ( log == null )
 79  
         {
 80  0
             throw new IllegalArgumentException( "missing logger" );
 81  
         }
 82  8
         this.log = log;
 83  8
         scriptInterpreters = new LinkedHashMap<String, ScriptInterpreter>();
 84  8
         scriptInterpreters.put( "bsh", new BeanShellScriptInterpreter() );
 85  8
         scriptInterpreters.put( "groovy", new GroovyScriptInterpreter() );
 86  8
         globalVariables = new HashMap<String, Object>();
 87  8
         classPath = new ArrayList<String>();
 88  8
     }
 89  
 
 90  
     public void addScriptInterpreter( String id, ScriptInterpreter scriptInterpreter )
 91  
     {
 92  0
         scriptInterpreters.put( id, scriptInterpreter );
 93  0
     }
 94  
 
 95  
     /**
 96  
      * Gets the mojo logger.
 97  
      *
 98  
      * @return The mojo logger, never <code>null</code>.
 99  
      */
 100  
     private Log getLog()
 101  
     {
 102  16
         return log;
 103  
     }
 104  
 
 105  
     /**
 106  
      * Sets a global variable for the script interpeter.
 107  
      *
 108  
      * @param name  The name of the variable, must not be <code>null</code>.
 109  
      * @param value The value of the variable, may be <code>null</code>.
 110  
      */
 111  
     public void setGlobalVariable( String name, Object value )
 112  
     {
 113  6
         this.globalVariables.put( name, value );
 114  6
     }
 115  
 
 116  
     /**
 117  
      * Sets the additional class path for the hook scripts. Note that the provided list is copied, so any later changes
 118  
      * will not affect the scripts.
 119  
      *
 120  
      * @param classPath The additional class path for the script interpreter, may be <code>null</code> or empty if only
 121  
      *                  the plugin realm should be used for the script evaluation. If specified, this class path will precede
 122  
      *                  the artifacts from the plugin class path.
 123  
      */
 124  
     public void setClassPath( List<String> classPath )
 125  
     {
 126  0
         this.classPath = ( classPath != null ) ? new ArrayList<String>( classPath ) : new ArrayList<String>();
 127  0
     }
 128  
 
 129  
     /**
 130  
      * Sets the file encoding of the hook scripts.
 131  
      *
 132  
      * @param encoding The file encoding of the hook scripts, may be <code>null</code> or empty to use the platform's
 133  
      *                 default encoding.
 134  
      */
 135  
     public void setScriptEncoding( String encoding )
 136  
     {
 137  0
         this.encoding = StringUtils.isNotEmpty( encoding ) ? encoding : null;
 138  0
     }
 139  
 
 140  
     /**
 141  
      * Runs the specified hook script.
 142  
      *
 143  
      * @param scriptDescription  The description of the script to use for logging, must not be <code>null</code>.
 144  
      * @param basedir            The base directory of the project, must not be <code>null</code>.
 145  
      * @param relativeScriptPath The path to the script relative to the project base directory, may be <code>null</code>
 146  
      *                           to skip the script execution.
 147  
      * @param context            The key-value storage used to share information between hook scripts, may be <code>null</code>.
 148  
      * @param logger             The logger to redirect the script output to, may be <code>null</code> to use stdout/stderr.
 149  
      * @param stage              The stage of the build job the script is invoked in, must not be <code>null</code>. This is for logging purpose only.
 150  
      * @param failOnException    If <code>true</code> and the script throws an exception, then a {@link RunFailureException}
 151  
      *                           will be thrown, otherwise a {@link RunErrorException} will be thrown on script exception.
 152  
      * @throws IOException         If an I/O error occurred while reading the script file.
 153  
      * @throws RunFailureException If the script did not return <code>true</code> of threw an exception.
 154  
      */
 155  
     public void run( final String scriptDescription, final File basedir, final String relativeScriptPath,
 156  
                      final Map<String, ? extends Object> context, final ExecutionLogger logger, String stage,
 157  
                      boolean failOnException )
 158  
         throws IOException, RunFailureException
 159  
     {
 160  4
         if ( relativeScriptPath == null )
 161  
         {
 162  0
             getLog().debug( "relativeScriptPath is null: not executing script" );
 163  0
             return;
 164  
         }
 165  
 
 166  4
         final File scriptFile = resolveScript( new File( basedir, relativeScriptPath ) );
 167  
 
 168  4
         if ( !scriptFile.exists() )
 169  
         {
 170  0
             getLog().debug( "no script found in directory: " + basedir.getAbsolutePath() );
 171  0
             return;
 172  
         }
 173  
 
 174  4
         String path = scriptFile.getAbsolutePath();
 175  4
         getLog().info( "run script " + relativeScriptPath + path.substring( path.lastIndexOf( '.' ) ) );
 176  
 
 177  4
         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
 178  4
     }
 179  
 
 180  
     /**
 181  
      * Runs the specified hook script.
 182  
      *
 183  
      * @param scriptDescription The description of the script to use for logging, must not be <code>null</code>.
 184  
      * @param scriptFile        The path to the script, may be <code>null</code> to skip the script execution.
 185  
      * @param context           The key-value storage used to share information between hook scripts, may be <code>null</code>.
 186  
      * @param logger            The logger to redirect the script output to, may be <code>null</code> to use stdout/stderr.
 187  
      * @param stage             The stage of the build job the script is invoked in, must not be <code>null</code>. This is for logging purpose only.
 188  
      * @param failOnException   If <code>true</code> and the script throws an exception, then a {@link RunFailureException}
 189  
      *                          will be thrown, otherwise a {@link RunErrorException} will be thrown on script exception.
 190  
      * @throws IOException         If an I/O error occurred while reading the script file.
 191  
      * @throws RunFailureException If the script did not return <code>true</code> of threw an exception.
 192  
      */
 193  
     public void run( final String scriptDescription, File scriptFile, final Map<String, ? extends Object> context,
 194  
                      final ExecutionLogger logger, String stage, boolean failOnException )
 195  
         throws IOException, RunFailureException
 196  
     {
 197  
 
 198  4
         if ( !scriptFile.exists() )
 199  
         {
 200  0
             getLog().debug( "scriptFile not found in directory: " + scriptFile.getAbsolutePath() );
 201  0
             return;
 202  
         }
 203  
 
 204  4
         getLog().info( "run script " + scriptFile.getAbsolutePath() );
 205  
 
 206  4
         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
 207  4
     }
 208  
 
 209  
     private void executeRun( final String scriptDescription, File scriptFile,
 210  
                              final Map<String, ? extends Object> context, final ExecutionLogger logger, String stage,
 211  
                              boolean failOnException )
 212  
         throws IOException, RunFailureException
 213  
     {
 214  8
         Map<String, Object> globalVariables = new HashMap<String, Object>( this.globalVariables );
 215  8
         globalVariables.put( "basedir", scriptFile.getParentFile() );
 216  8
         globalVariables.put( "context", context );
 217  
 
 218  8
         PrintStream out = ( logger != null ) ? logger.getPrintStream() : null;
 219  
 
 220  8
         ScriptInterpreter interpreter = getInterpreter( scriptFile );
 221  8
         if ( getLog().isDebugEnabled() )
 222  
         {
 223  0
             String name = interpreter.getClass().getName();
 224  0
             name = name.substring( name.lastIndexOf( '.' ) + 1 );
 225  0
             getLog().debug( "Running script with " + name + ": " + scriptFile );
 226  
         }
 227  
 
 228  
         String script;
 229  
         try
 230  
         {
 231  8
             script = FileUtils.fileRead( scriptFile, encoding );
 232  
         }
 233  0
         catch ( IOException e )
 234  
         {
 235  0
             String errorMessage =
 236  
                 "error reading " + scriptDescription + " " + scriptFile.getPath() + ", " + e.getMessage();
 237  0
             IOException ioException = new IOException( errorMessage );
 238  0
             ioException.initCause( e );
 239  0
             throw ioException;
 240  8
         }
 241  
 
 242  
         Object result;
 243  
         try
 244  
         {
 245  8
             if ( logger != null )
 246  
             {
 247  8
                 logger.consumeLine( "Running " + scriptDescription + ": " + scriptFile );
 248  
             }
 249  8
             result = interpreter.evaluateScript( script, classPath, globalVariables, out );
 250  8
             if ( logger != null )
 251  
             {
 252  8
                 logger.consumeLine( "Finished " + scriptDescription + ": " + scriptFile );
 253  
             }
 254  
         }
 255  0
         catch ( ScriptEvaluationException e )
 256  
         {
 257  0
             Throwable t = ( e.getCause() != null ) ? e.getCause() : e;
 258  0
             String msg = ( t.getMessage() != null ) ? t.getMessage() : t.toString();
 259  0
             if ( getLog().isDebugEnabled() )
 260  
             {
 261  0
                 String errorMessage = "Error evaluating " + scriptDescription + " " + scriptFile.getPath() + ", " + t;
 262  0
                 getLog().debug( errorMessage, t );
 263  
             }
 264  0
             if ( logger != null )
 265  
             {
 266  0
                 t.printStackTrace( logger.getPrintStream() );
 267  
             }
 268  0
             if ( failOnException )
 269  
             {
 270  0
                 throw new RunFailureException( "The " + scriptDescription + " did not succeed. " + msg, stage );
 271  
             }
 272  
             else
 273  
             {
 274  0
                 throw new RunErrorException( "The " + scriptDescription + " did not succeed. " + msg, stage, t );
 275  
             }
 276  8
         }
 277  
 
 278  8
         if ( !( result == null || Boolean.TRUE.equals( result ) || "true".equals( result ) ) )
 279  
         {
 280  0
             throw new RunFailureException( "The " + scriptDescription + " returned " + result + ".", stage );
 281  
         }
 282  8
     }
 283  
 
 284  
     /**
 285  
      * Gets the effective path to the specified script. For convenience, we allow to specify a script path as "verify"
 286  
      * and have the plugin auto-append the file extension to search for "verify.bsh" and "verify.groovy".
 287  
      *
 288  
      * @param scriptFile The script file to resolve, may be <code>null</code>.
 289  
      * @return The effective path to the script file or <code>null</code> if the input was <code>null</code>.
 290  
      */
 291  
     private File resolveScript( File scriptFile )
 292  
     {
 293  4
         if ( scriptFile != null && !scriptFile.exists() )
 294  
         {
 295  4
             for ( String ext : this.scriptInterpreters.keySet() )
 296  
             {
 297  6
                 File candidateFile = new File( scriptFile.getPath() + '.' + ext );
 298  6
                 if ( candidateFile.exists() )
 299  
                 {
 300  4
                     scriptFile = candidateFile;
 301  4
                     break;
 302  
                 }
 303  2
             }
 304  
         }
 305  4
         return scriptFile;
 306  
     }
 307  
 
 308  
     /**
 309  
      * Determines the script interpreter for the specified script file by looking at its file extension. In this
 310  
      * context, file extensions are considered case-insensitive. For backward compatibility with plugin versions 1.2-,
 311  
      * the BeanShell interpreter will be used for any unrecognized extension.
 312  
      *
 313  
      * @param scriptFile The script file for which to determine an interpreter, must not be <code>null</code>.
 314  
      * @return The script interpreter for the file, never <code>null</code>.
 315  
      */
 316  
     private ScriptInterpreter getInterpreter( File scriptFile )
 317  
     {
 318  8
         String ext = FileUtils.extension( scriptFile.getName() ).toLowerCase( Locale.ENGLISH );
 319  8
         ScriptInterpreter interpreter = scriptInterpreters.get( ext );
 320  8
         if ( interpreter == null )
 321  
         {
 322  0
             interpreter = scriptInterpreters.get( "bsh" );
 323  
         }
 324  8
         return interpreter;
 325  
     }
 326  
 
 327  
 }