View Javadoc

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 }