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 }