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