View Javadoc
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 }