View Javadoc

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      {
78          if ( log == null )
79          {
80              throw new IllegalArgumentException( "missing logger" );
81          }
82          this.log = log;
83          scriptInterpreters = new LinkedHashMap<String, ScriptInterpreter>();
84          scriptInterpreters.put( "bsh", new BeanShellScriptInterpreter() );
85          scriptInterpreters.put( "groovy", new GroovyScriptInterpreter() );
86          globalVariables = new HashMap<String, Object>();
87          classPath = new ArrayList<String>();
88      }
89  
90      public void addScriptInterpreter( String id, ScriptInterpreter scriptInterpreter )
91      {
92          scriptInterpreters.put( id, scriptInterpreter );
93      }
94  
95      /**
96       * Gets the mojo logger.
97       *
98       * @return The mojo logger, never <code>null</code>.
99       */
100     private Log getLog()
101     {
102         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         this.globalVariables.put( name, value );
114     }
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         this.classPath = ( classPath != null ) ? new ArrayList<String>( classPath ) : new ArrayList<String>();
127     }
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         this.encoding = StringUtils.isNotEmpty( encoding ) ? encoding : null;
138     }
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         if ( relativeScriptPath == null )
161         {
162             getLog().debug( "relativeScriptPath is null: not executing script" );
163             return;
164         }
165 
166         final File scriptFile = resolveScript( new File( basedir, relativeScriptPath ) );
167 
168         if ( !scriptFile.exists() )
169         {
170             getLog().debug( "no script found in directory: " + basedir.getAbsolutePath() );
171             return;
172         }
173 
174         String path = scriptFile.getAbsolutePath();
175         getLog().info( "run script " + relativeScriptPath + path.substring( path.lastIndexOf( '.' ) ) );
176 
177         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
178     }
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         if ( !scriptFile.exists() )
199         {
200             getLog().debug( "scriptFile not found in directory: " + scriptFile.getAbsolutePath() );
201             return;
202         }
203 
204         getLog().info( "run script " + scriptFile.getAbsolutePath() );
205 
206         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
207     }
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         Map<String, Object> globalVariables = new HashMap<String, Object>( this.globalVariables );
215         globalVariables.put( "basedir", scriptFile.getParentFile() );
216         globalVariables.put( "context", context );
217 
218         PrintStream out = ( logger != null ) ? logger.getPrintStream() : null;
219 
220         ScriptInterpreter interpreter = getInterpreter( scriptFile );
221         if ( getLog().isDebugEnabled() )
222         {
223             String name = interpreter.getClass().getName();
224             name = name.substring( name.lastIndexOf( '.' ) + 1 );
225             getLog().debug( "Running script with " + name + ": " + scriptFile );
226         }
227 
228         String script;
229         try
230         {
231             script = FileUtils.fileRead( scriptFile, encoding );
232         }
233         catch ( IOException e )
234         {
235             String errorMessage =
236                 "error reading " + scriptDescription + " " + scriptFile.getPath() + ", " + e.getMessage();
237             IOException ioException = new IOException( errorMessage );
238             ioException.initCause( e );
239             throw ioException;
240         }
241 
242         Object result;
243         try
244         {
245             if ( logger != null )
246             {
247                 logger.consumeLine( "Running " + scriptDescription + ": " + scriptFile );
248             }
249             result = interpreter.evaluateScript( script, classPath, globalVariables, out );
250             if ( logger != null )
251             {
252                 logger.consumeLine( "Finished " + scriptDescription + ": " + scriptFile );
253             }
254         }
255         catch ( ScriptEvaluationException e )
256         {
257             Throwable t = ( e.getCause() != null ) ? e.getCause() : e;
258             String msg = ( t.getMessage() != null ) ? t.getMessage() : t.toString();
259             if ( getLog().isDebugEnabled() )
260             {
261                 String errorMessage = "Error evaluating " + scriptDescription + " " + scriptFile.getPath() + ", " + t;
262                 getLog().debug( errorMessage, t );
263             }
264             if ( logger != null )
265             {
266                 t.printStackTrace( logger.getPrintStream() );
267             }
268             if ( failOnException )
269             {
270                 throw new RunFailureException( "The " + scriptDescription + " did not succeed. " + msg, stage );
271             }
272             else
273             {
274                 throw new RunErrorException( "The " + scriptDescription + " did not succeed. " + msg, stage, t );
275             }
276         }
277 
278         if ( !( result == null || Boolean.TRUE.equals( result ) || "true".equals( result ) ) )
279         {
280             throw new RunFailureException( "The " + scriptDescription + " returned " + result + ".", stage );
281         }
282     }
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         if ( scriptFile != null && !scriptFile.exists() )
294         {
295             for ( String ext : this.scriptInterpreters.keySet() )
296             {
297                 File candidateFile = new File( scriptFile.getPath() + '.' + ext );
298                 if ( candidateFile.exists() )
299                 {
300                     scriptFile = candidateFile;
301                     break;
302                 }
303             }
304         }
305         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         String ext = FileUtils.extension( scriptFile.getName() ).toLowerCase( Locale.ENGLISH );
319         ScriptInterpreter interpreter = scriptInterpreters.get( ext );
320         if ( interpreter == null )
321         {
322             interpreter = scriptInterpreters.get( "bsh" );
323         }
324         return interpreter;
325     }
326 
327 }