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