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.apache.maven.shared.utils.io.FileUtils;
24  import org.apache.maven.shared.utils.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 1797598 2017-06-04 18:41:18Z 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 interpreter.
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 (after resolution).
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 and may not have extensions (resolution will search).
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
150      *            logging purpose only.
151      * @param failOnException If <code>true</code> and the script throws an exception, then a
152      *            {@link RunFailureException} will be thrown, otherwise a {@link RunErrorException} will be thrown on
153      *            script exception.
154      * @throws IOException If an I/O error occurred while reading the script file.
155      * @throws RunFailureException If the script did not return <code>true</code> of threw an exception.
156      */
157     public void run( final String scriptDescription, final File basedir, final String relativeScriptPath,
158                      final Map<String, ? extends Object> context, final ExecutionLogger logger, String stage,
159                      boolean failOnException )
160         throws IOException, RunFailureException
161     {
162         if ( relativeScriptPath == null )
163         {
164             getLog().debug( scriptDescription + ": relativeScriptPath is null, not executing script" );
165             return;
166         }
167 
168         final File scriptFile = resolveScript( new File( basedir, relativeScriptPath ) );
169 
170         if ( !scriptFile.exists() )
171         {
172             getLog().debug( scriptDescription + ": no script '" + relativeScriptPath + "' found in directory "
173                 + basedir.getAbsolutePath() );
174             return;
175         }
176 
177         getLog().info( "run " + scriptDescription + ' ' + relativeScriptPath + '.'
178             + FileUtils.extension( scriptFile.getAbsolutePath() ) );
179 
180         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
181     }
182 
183     /**
184      * Runs the specified hook script.
185      *
186      * @param scriptDescription The description of the script to use for logging, must not be <code>null</code>.
187      * @param scriptFile The path to the script, may be <code>null</code> to skip the script execution.
188      * @param context The key-value storage used to share information between hook scripts, may be <code>null</code>.
189      * @param logger The logger to redirect the script output to, may be <code>null</code> to use stdout/stderr.
190      * @param stage The stage of the build job the script is invoked in, must not be <code>null</code>. This is for
191      *            logging purpose only.
192      * @param failOnException If <code>true</code> and the script throws an exception, then a
193      *            {@link RunFailureException} will be thrown, otherwise a {@link RunErrorException} will be thrown on
194      *            script exception.
195      * @throws IOException If an I/O error occurred while reading the script file.
196      * @throws RunFailureException If the script did not return <code>true</code> of threw an exception.
197      */
198     public void run( final String scriptDescription, File scriptFile, final Map<String, ? extends Object> context,
199                      final ExecutionLogger logger, String stage, boolean failOnException )
200         throws IOException, RunFailureException
201     {
202 
203         if ( !scriptFile.exists() )
204         {
205             getLog().debug( scriptDescription + ": script file not found in directory "
206                 + scriptFile.getAbsolutePath() );
207             return;
208         }
209 
210         getLog().info( "run " + scriptDescription + ' ' + scriptFile.getAbsolutePath() );
211 
212         executeRun( scriptDescription, scriptFile, context, logger, stage, failOnException );
213     }
214 
215     private void executeRun( final String scriptDescription, File scriptFile,
216                              final Map<String, ? extends Object> context, final ExecutionLogger logger, String stage,
217                              boolean failOnException )
218         throws IOException, RunFailureException
219     {
220         Map<String, Object> globalVariables = new HashMap<String, Object>( this.globalVariables );
221         globalVariables.put( "basedir", scriptFile.getParentFile() );
222         globalVariables.put( "context", context );
223 
224         ScriptInterpreter interpreter = getInterpreter( scriptFile );
225         if ( getLog().isDebugEnabled() )
226         {
227             String name = interpreter.getClass().getName();
228             name = name.substring( name.lastIndexOf( '.' ) + 1 );
229             getLog().debug( "Running script with " + name + ": " + scriptFile );
230         }
231 
232         String script;
233         try
234         {
235             script = FileUtils.fileRead( scriptFile, encoding );
236         }
237         catch ( IOException e )
238         {
239             String errorMessage =
240                 "error reading " + scriptDescription + " " + scriptFile.getPath() + ", " + e.getMessage();
241             IOException ioException = new IOException( errorMessage );
242             ioException.initCause( e );
243             throw ioException;
244         }
245 
246         Object result;
247         try
248         {
249             if ( logger != null )
250             {
251                 logger.consumeLine( "Running " + scriptDescription + ": " + scriptFile );
252             }
253 
254             PrintStream out = ( logger != null ) ? logger.getPrintStream() : null;
255 
256             result = interpreter.evaluateScript( script, classPath, globalVariables, out );
257             if ( logger != null )
258             {
259                 logger.consumeLine( "Finished " + scriptDescription + ": " + scriptFile );
260             }
261         }
262         catch ( ScriptEvaluationException e )
263         {
264             Throwable t = ( e.getCause() != null ) ? e.getCause() : e;
265             String msg = ( t.getMessage() != null ) ? t.getMessage() : t.toString();
266             if ( getLog().isDebugEnabled() )
267             {
268                 String errorMessage = "Error evaluating " + scriptDescription + " " + scriptFile.getPath() + ", " + t;
269                 getLog().debug( errorMessage, t );
270             }
271             if ( logger != null )
272             {
273                 t.printStackTrace( logger.getPrintStream() );
274             }
275             if ( failOnException )
276             {
277                 throw new RunFailureException( "The " + scriptDescription + " did not succeed. " + msg, stage );
278             }
279             else
280             {
281                 throw new RunErrorException( "The " + scriptDescription + " did not succeed. " + msg, stage, t );
282             }
283         }
284 
285         if ( !( result == null || Boolean.TRUE.equals( result ) || "true".equals( result ) ) )
286         {
287             throw new RunFailureException( "The " + scriptDescription + " returned " + result + ".", stage );
288         }
289     }
290 
291     /**
292      * Gets the effective path to the specified script. For convenience, we allow to specify a script path as "verify"
293      * and have the plugin auto-append the file extension to search for "verify.bsh" and "verify.groovy".
294      *
295      * @param scriptFile The script file to resolve, may be <code>null</code>.
296      * @return The effective path to the script file or <code>null</code> if the input was <code>null</code>.
297      */
298     private File resolveScript( File scriptFile )
299     {
300         if ( scriptFile != null && !scriptFile.exists() )
301         {
302             for ( String ext : this.scriptInterpreters.keySet() )
303             {
304                 File candidateFile = new File( scriptFile.getPath() + '.' + ext );
305                 if ( candidateFile.exists() )
306                 {
307                     scriptFile = candidateFile;
308                     break;
309                 }
310             }
311         }
312         return scriptFile;
313     }
314 
315     /**
316      * Determines the script interpreter for the specified script file by looking at its file extension. In this
317      * context, file extensions are considered case-insensitive. For backward compatibility with plugin versions 1.2-,
318      * the BeanShell interpreter will be used for any unrecognized extension.
319      *
320      * @param scriptFile The script file for which to determine an interpreter, must not be <code>null</code>.
321      * @return The script interpreter for the file, never <code>null</code>.
322      */
323     private ScriptInterpreter getInterpreter( File scriptFile )
324     {
325         String ext = FileUtils.extension( scriptFile.getName() ).toLowerCase( Locale.ENGLISH );
326         ScriptInterpreter interpreter = scriptInterpreters.get( ext );
327         if ( interpreter == null )
328         {
329             interpreter = scriptInterpreters.get( "bsh" );
330         }
331         return interpreter;
332     }
333 
334 }