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 }