View Javadoc
1   /*
2    * Copyright (C) 2011 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.codehaus.gmavenplus.mojo;
18  
19  import org.apache.maven.artifact.DependencyResolutionRequiredException;
20  import org.apache.maven.plugin.MojoExecutionException;
21  import org.apache.maven.plugins.annotations.Mojo;
22  import org.apache.maven.plugins.annotations.Parameter;
23  import org.apache.maven.plugins.annotations.ResolutionScope;
24  import org.codehaus.gmavenplus.model.internal.Version;
25  import org.codehaus.gmavenplus.util.FileUtils;
26  import org.codehaus.gmavenplus.util.NoExitSecurityManager;
27  
28  import java.io.BufferedReader;
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.InputStreamReader;
33  import java.io.Reader;
34  import java.lang.reflect.InvocationTargetException;
35  import java.lang.reflect.Method;
36  import java.net.MalformedURLException;
37  import java.net.URL;
38  import java.net.URLConnection;
39  
40  import static org.codehaus.gmavenplus.util.ReflectionUtils.findConstructor;
41  import static org.codehaus.gmavenplus.util.ReflectionUtils.findMethod;
42  import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeConstructor;
43  import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeMethod;
44  
45  
46  /**
47   * Executes Groovy scripts (in the pom or external), bound to the current project.
48   * Note that this mojo requires Groovy >= 1.5.0.
49   * Note that it references the plugin classloader to pull in dependencies Groovy didn't include
50   * (for things like Ant for AntBuilder, Ivy for @grab, and Jansi for Groovysh).
51   *
52   * @author Keegan Witt
53   * @since 1.0-beta-1
54   */
55  @Mojo(name = "execute", requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true)
56  public class ExecuteMojo extends AbstractToolsMojo {
57  
58      /**
59       * Groovy 4.0.0-RC-1 version.
60       */
61      protected static final Version GROOVY_4_0_0_RC_1 = new Version(4, 0, 0, "RC-1");
62  
63      /**
64       * Groovy 1.7.0 version.
65       */
66      protected static final Version GROOVY_1_7_0 = new Version(1, 7, 0);
67  
68      /**
69       * Groovy scripts to run (in order). Can be a script body, a {@link java.net.URL URL} to a script
70       * (local or remote), or a filename.
71       */
72      @Parameter(required = true, property = "scripts")
73      protected String[] scripts;
74  
75      /**
76       * Whether to continue executing remaining scripts when a script fails.
77       */
78      @Parameter(defaultValue = "false", property = "continueExecuting")
79      protected boolean continueExecuting;
80  
81      /**
82       * The encoding of script files/URLs.
83       *
84       * @since 1.0-beta-2
85       */
86      @Parameter(defaultValue = "${project.build.sourceEncoding}")
87      protected String sourceEncoding;
88  
89      /**
90       * Flag to allow script execution to be skipped.
91       *
92       * @since 1.9.1
93       */
94      @Parameter(defaultValue = "false", property = "skipScriptExecution")
95      protected boolean skipScriptExecution;
96  
97      /**
98       * The timeout to use for URL connections.
99       *
100      * @since 4.1.0
101      */
102     @Parameter(defaultValue = "0", property = "urlConnectionTimeout")
103     protected int urlConnectionTimeout;
104 
105     /**
106      * The timeout to use for URL reading.
107      *
108      * @since 4.1.0
109      */
110     @Parameter(defaultValue = "0", property = "urlReadTimeout")
111     protected int urlReadTimeout;
112 
113     /**
114      * Executes this mojo.
115      *
116      * @throws MojoExecutionException If an unexpected problem occurs (causes a "BUILD ERROR" message to be displayed)
117      */
118     @Override
119     public void execute() throws MojoExecutionException {
120         doExecute();
121     }
122 
123     /**
124      * Does the actual execution.
125      *
126      * @throws MojoExecutionException If an unexpected problem occurs (causes a "BUILD ERROR" message to be displayed)
127      */
128     protected synchronized void doExecute() throws MojoExecutionException {
129         if (skipScriptExecution) {
130             getLog().info("Skipping script execution because ${skipScriptExecution} was set to true.");
131             return;
132         }
133 
134         if (scripts == null || scripts.length == 0) {
135             getLog().info("No scripts specified for execution. Skipping.");
136             return;
137         }
138 
139         try {
140             setupClassWrangler(project.getTestClasspathElements(), includeClasspath);
141         } catch (MalformedURLException e) {
142             throw new MojoExecutionException("Unable to add project test dependencies to classpath.", e);
143         } catch (DependencyResolutionRequiredException e) {
144             throw new MojoExecutionException("Test dependencies weren't resolved.", e);
145         }
146 
147         logPluginClasspath();
148         classWrangler.logGroovyVersion(mojoExecution.getMojoDescriptor().getGoal());
149 
150         try {
151             getLog().debug("Project test classpath:\n" + project.getTestClasspathElements());
152         } catch (DependencyResolutionRequiredException e) {
153             getLog().debug("Unable to log project test classpath");
154         }
155 
156         if (!groovyVersionSupportsAction()) {
157             getLog().error("Your Groovy version (" + classWrangler.getGroovyVersionString() + ") doesn't support script execution. The minimum version of Groovy required is " + minGroovyVersion + ". Skipping script execution.");
158             return;
159         }
160 
161         final SecurityManager defaultSecurityManager = System.getSecurityManager();
162         try {
163             if (!allowSystemExits) {
164                 getLog().warn("JEP 411 deprecated Security Manager in Java 17 for removal. Therefore `allowSystemExits` is also deprecated for removal.");
165                 try {
166                     System.setSecurityManager(new NoExitSecurityManager());
167                 } catch (UnsupportedOperationException e) {
168                     getLog().warn("Attempted to use Security Manager in a JVM where it's disabled by default. You might try `-Djava.security.manager=allow` to override this.");
169                 }
170             }
171 
172             // get classes we need with reflection
173             Class<?> groovyShellClass = classWrangler.getClass("groovy.lang.GroovyShell");
174 
175             // create a GroovyShell to run scripts in
176             Object shell = setupShell(groovyShellClass);
177 
178             // run the scripts
179             executeScripts(groovyShellClass, shell);
180         } catch (ClassNotFoundException e) {
181             throw new MojoExecutionException("Unable to get a Groovy class from classpath (" + e.getMessage() + "). Do you have Groovy as a compile dependency in your project or the plugin?", e);
182         } catch (InvocationTargetException e) {
183             throw new MojoExecutionException("Error occurred while calling a method on a Groovy class from classpath.", e);
184         } catch (InstantiationException e) {
185             throw new MojoExecutionException("Error occurred while instantiating a Groovy class from classpath.", e);
186         } catch (IllegalAccessException e) {
187             throw new MojoExecutionException("Unable to access a method on a Groovy class from classpath.", e);
188         } finally {
189             if (!allowSystemExits) {
190                 try {
191                     System.setSecurityManager(defaultSecurityManager);
192                 } catch (UnsupportedOperationException e) {
193                     getLog().warn("Attempted to use Security Manager in a JVM where it's disabled by default. You might try `-Djava.security.manager=allow` to override this.");
194                 }
195             }
196         }
197     }
198 
199     /**
200      * Instantiates a new groovy.lang.GroovyShell object.
201      *
202      * @param groovyShellClass the groovy.lang.GroovyShell class
203      * @return a new groovy.lang.GroovyShell object
204      * @throws InvocationTargetException when a reflection invocation needed for shell configuration cannot be completed
205      * @throws IllegalAccessException    when a method needed for shell configuration cannot be accessed
206      * @throws InstantiationException    when a class needed for shell configuration cannot be instantiated
207      * @throws ClassNotFoundException    when a class needed for shell configuration cannot be found
208      */
209     protected Object setupShell(final Class<?> groovyShellClass) throws InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException {
210         Object shell;
211         if (sourceEncoding != null) {
212             Class<?> compilerConfigurationClass = classWrangler.getClass("org.codehaus.groovy.control.CompilerConfiguration");
213             Object compilerConfiguration = invokeConstructor(findConstructor(compilerConfigurationClass));
214             invokeMethod(findMethod(compilerConfigurationClass, "setSourceEncoding", String.class), compilerConfiguration, sourceEncoding);
215             shell = invokeConstructor(findConstructor(groovyShellClass, ClassLoader.class, compilerConfigurationClass), classWrangler.getClassLoader(), compilerConfiguration);
216         } else {
217             shell = invokeConstructor(findConstructor(groovyShellClass, ClassLoader.class), classWrangler.getClassLoader());
218         }
219         initializeProperties();
220         Method setProperty = findMethod(groovyShellClass, "setProperty", String.class, Object.class);
221         if (bindPropertiesToSeparateVariables) {
222             for (Object k : properties.keySet()) {
223                 invokeMethod(setProperty, shell, k, properties.get(k));
224             }
225         } else {
226             if (groovyOlderThan(GROOVY_4_0_0_RC_1)) {
227                 invokeMethod(setProperty, shell, "properties", properties);
228             } else {
229                 throw new IllegalArgumentException("properties is a read-only property in Groovy " + GROOVY_4_0_0_RC_1 + " and later.");
230             }
231         }
232 
233         return shell;
234     }
235 
236     /**
237      * Executes the configured scripts.
238      *
239      * @param groovyShellClass the groovy.lang.GroovyShell class
240      * @param shell            a groovy.lag.GroovyShell object
241      * @throws InvocationTargetException when a reflection invocation needed for script execution cannot be completed
242      * @throws IllegalAccessException    when a method needed for script execution cannot be accessed
243      * @throws MojoExecutionException    when an exception occurred during script execution (causes a "BUILD ERROR" message to be displayed)
244      */
245     protected void executeScripts(final Class<?> groovyShellClass, final Object shell) throws InvocationTargetException, IllegalAccessException, MojoExecutionException {
246         int scriptNum = 1;
247         for (String script : scripts) {
248             try {
249                 // TODO: try as file first, then as URL?
250                 try {
251                     // it's a URL to a script
252                     executeScriptFromUrl(groovyShellClass, shell, script);
253                 } catch (MalformedURLException e) {
254                     // it's not a URL to a script, try as a filename
255                     File scriptFile = new File(script);
256                     if (scriptFile.isFile()) {
257                         getLog().info("Running Groovy script from " + scriptFile.getCanonicalPath() + ".");
258                         Method evaluateFile = findMethod(groovyShellClass, "evaluate", File.class);
259                         invokeMethod(evaluateFile, shell, scriptFile);
260                     } else {
261                         // it's neither a filename or URL, treat as a script body
262                         Method evaluateString = findMethod(groovyShellClass, "evaluate", String.class);
263                         invokeMethod(evaluateString, shell, script);
264                     }
265                 }
266             } catch (IOException ioe) {
267                 if (continueExecuting) {
268                     getLog().error("An Exception occurred while executing script " + scriptNum + ". Continuing to execute remaining scripts.", ioe);
269                 } else {
270                     throw new MojoExecutionException("An Exception occurred while executing script " + scriptNum + ".", ioe);
271                 }
272             }
273             scriptNum++;
274         }
275     }
276 
277     /**
278      * Executes a script at a URL location.
279      *
280      * @param groovyShellClass the GroovyShell class
281      * @param shell            a groovy.lag.GroovyShell object
282      * @param script           the script URL to execute
283      * @throws IOException               when the stream can't be opened on the URL
284      * @throws InvocationTargetException when a reflection invocation needed for script execution cannot be completed
285      * @throws IllegalAccessException    when a method needed for script execution cannot be accessed
286      */
287     protected void executeScriptFromUrl(Class<?> groovyShellClass, Object shell, String script) throws IOException, InvocationTargetException, IllegalAccessException {
288         URLConnection urlConnection = new URL(script).openConnection();
289         if (urlConnectionTimeout > 0) {
290             urlConnection.setConnectTimeout(urlConnectionTimeout);
291         }
292         if (urlReadTimeout > 0) {
293             urlConnection.setReadTimeout(urlReadTimeout);
294         }
295         getLog().info("Running Groovy script from " + urlConnection.getURL() + ".");
296         if (groovyAtLeast(GROOVY_1_7_0)) {
297             Method evaluateUrlWithReader = findMethod(groovyShellClass, "evaluate", Reader.class);
298             BufferedReader reader = null;
299             try {
300                 if (sourceEncoding != null) {
301                     reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), sourceEncoding));
302                 } else {
303                     reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
304                 }
305                 invokeMethod(evaluateUrlWithReader, shell, reader);
306             } finally {
307                 FileUtils.closeQuietly(reader);
308             }
309         } else {
310             Method evaluateUrlWithStream = findMethod(groovyShellClass, "evaluate", InputStream.class);
311             InputStream inputStream = null;
312             try {
313                 if (sourceEncoding != null) {
314                     getLog().warn("Source encoding does not apply to Groovy versions previous to 1.7.0, ignoring.");
315                 }
316                 inputStream = urlConnection.getInputStream();
317                 invokeMethod(evaluateUrlWithStream, shell, urlConnection.getInputStream());
318             } finally {
319                 FileUtils.closeQuietly(inputStream);
320             }
321         }
322     }
323 
324 }