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