View Javadoc
1   /*
2    * Copyright 2014 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.plugin.MojoFailureException;
22  import org.apache.maven.plugins.annotations.Mojo;
23  import org.apache.maven.plugins.annotations.Parameter;
24  import org.apache.maven.plugins.annotations.ResolutionScope;
25  import org.codehaus.gmavenplus.util.NoExitSecurityManager;
26  
27  import java.io.File;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import java.net.MalformedURLException;
31  import java.util.Set;
32  
33  import static org.codehaus.gmavenplus.mojo.ExecuteMojo.GROOVY_4_0_0_RC_1;
34  import static org.codehaus.gmavenplus.util.ReflectionUtils.findConstructor;
35  import static org.codehaus.gmavenplus.util.ReflectionUtils.findField;
36  import static org.codehaus.gmavenplus.util.ReflectionUtils.findMethod;
37  import static org.codehaus.gmavenplus.util.ReflectionUtils.getField;
38  import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeConstructor;
39  import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeMethod;
40  
41  
42  /**
43   * Launches a Groovy console window bound to the current project.
44   * Note that this mojo requires Groovy >= 1.5.0.
45   * Note that it references the plugin classloader to pull in dependencies Groovy didn't include
46   * (for things like Ant for AntBuilder, Ivy for @grab, and Jansi for Groovysh).
47   * Note that using the <code>ant</code> property requires Java 8, as the included Ant version was compiled for Java 8.
48   *
49   * @author Keegan Witt
50   * @since 1.1
51   */
52  @Mojo(name = "console", requiresDependencyResolution = ResolutionScope.TEST)
53  public class ConsoleMojo extends AbstractToolsMojo {
54  
55      /**
56       * Script file to load into console. Can also be a project property referring to a file.
57       *
58       * @since 1.10.1
59       */
60      @Parameter(property = "consoleScript")
61      protected String consoleScript;
62  
63      /**
64       * Executes this mojo.
65       *
66       * @throws MojoExecutionException If an unexpected problem occurs (causes a "BUILD ERROR" message to be displayed)
67       * @throws MojoFailureException   If unable to await console exit
68       */
69      @Override
70      public void execute() throws MojoExecutionException, MojoFailureException {
71          try {
72              setupClassWrangler(project.getTestClasspathElements(), includeClasspath);
73          } catch (MalformedURLException e) {
74              throw new MojoExecutionException("Unable to add project test dependencies to classpath.", e);
75          } catch (DependencyResolutionRequiredException e) {
76              throw new MojoExecutionException("Test dependencies weren't resolved.", e);
77          }
78  
79          logPluginClasspath();
80          classWrangler.logGroovyVersion(mojoExecution.getMojoDescriptor().getGoal());
81  
82          try {
83              getLog().debug("Project test classpath:\n" + project.getTestClasspathElements());
84          } catch (DependencyResolutionRequiredException e) {
85              getLog().debug("Unable to log project test classpath");
86          }
87  
88          if (groovyVersionSupportsAction()) {
89              final SecurityManager sm = System.getSecurityManager();
90              try {
91                  if (!allowSystemExits) {
92                      System.setSecurityManager(new NoExitSecurityManager());
93                  }
94  
95                  // get classes we need with reflection
96                  Class<?> consoleClass;
97                  try {
98                      consoleClass = classWrangler.getClass("groovy.console.ui.Console");
99                  } catch (ClassNotFoundException e) {
100                     consoleClass = classWrangler.getClass("groovy.ui.Console");
101                 }
102                 Class<?> bindingClass = classWrangler.getClass("groovy.lang.Binding");
103 
104                 // create console to run
105                 Object console = setupConsole(consoleClass, bindingClass);
106 
107                 // run the console
108                 invokeMethod(findMethod(consoleClass, "run"), console);
109 
110                 // TODO: for some reason instantiating AntBuilder before calling run() causes its stdout and stderr streams to not be captured by the Console
111                 bindAntBuilder(consoleClass, bindingClass, console);
112 
113                 // open script file
114                 loadScript(consoleClass, console);
115 
116                 // wait for console to be closed
117                 waitForConsoleClose();
118             } catch (ClassNotFoundException e) {
119                 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);
120             } catch (InvocationTargetException e) {
121                 if (e.getCause() instanceof NoClassDefFoundError && "org/apache/ivy/core/report/ResolveReport".equals(e.getCause().getMessage())) {
122                     throw new MojoExecutionException("Groovy 1.7.6 and 1.7.7 have a dependency on Ivy to run the console. Either change your Groovy version or add Ivy as a project or plugin dependency.", e);
123                 } else {
124                     throw new MojoExecutionException("Error occurred while calling a method on a Groovy class from classpath.", e);
125                 }
126             } catch (IllegalAccessException e) {
127                 throw new MojoExecutionException("Unable to access a method on a Groovy class from classpath.", e);
128             } catch (InstantiationException e) {
129                 throw new MojoExecutionException("Error occurred while instantiating a Groovy class from classpath.", e);
130             } finally {
131                 if (!allowSystemExits) {
132                     System.setSecurityManager(sm);
133                 }
134             }
135         } else {
136             getLog().error("Your Groovy version (" + classWrangler.getGroovyVersionString() + ") doesn't support running a console. The minimum version of Groovy required is " + minGroovyVersion + ". Skipping console startup.");
137         }
138     }
139 
140     protected void loadScript(Class<?> consoleClass, Object console) throws InvocationTargetException, IllegalAccessException {
141         if (consoleScript != null) {
142             Method loadScriptFile = findMethod(consoleClass, "loadScriptFile", File.class);
143             File consoleScriptFile = new File(consoleScript);
144             if (consoleScriptFile.isFile()) {
145                 invokeMethod(loadScriptFile, console, consoleScriptFile);
146             } else if (project.getProperties().containsKey(consoleScript)) {
147                 consoleScriptFile = new File(project.getProperties().getProperty(consoleScript));
148                 if (consoleScriptFile.isFile()) {
149                     invokeMethod(loadScriptFile, console, consoleScriptFile);
150                 } else {
151                     getLog().warn("consoleScript ('" + consoleScript + "') doesn't exist in project properties or as a file.");
152                 }
153             } else {
154                 getLog().warn("consoleScript ('" + consoleScript + "') doesn't exist in project properties or as a file.");
155             }
156         }
157     }
158 
159     /**
160      * Instantiates a groovy.ui.Console object.
161      *
162      * @param consoleClass the groovy.ui.Console class to use
163      * @param bindingClass the groovy.lang.Binding class to use
164      * @return a new groovy.ui.Console object
165      * @throws InvocationTargetException when a reflection invocation needed for instantiating a console object cannot be completed
166      * @throws IllegalAccessException    when a method needed for instantiating a console object cannot be accessed
167      * @throws InstantiationException    when a class needed for instantiating a console object cannot be instantiated
168      */
169     protected Object setupConsole(final Class<?> consoleClass, final Class<?> bindingClass) throws InvocationTargetException, IllegalAccessException, InstantiationException {
170         Object binding = invokeConstructor(findConstructor(bindingClass));
171         initializeProperties();
172         Method setVariable = findMethod(bindingClass, "setVariable", String.class, Object.class);
173         if (bindPropertiesToSeparateVariables) {
174             for (Object k : properties.keySet()) {
175                 invokeMethod(setVariable, binding, k, properties.get(k));
176             }
177         } else {
178             if (groovyOlderThan(GROOVY_4_0_0_RC_1)) {
179                 invokeMethod(setVariable, binding, "properties", properties);
180             } else {
181                 throw new IllegalArgumentException("properties is a read-only property in Groovy " + GROOVY_4_0_0_RC_1 + " and later.");
182             }
183         }
184 
185         return invokeConstructor(findConstructor(consoleClass, ClassLoader.class, bindingClass), classWrangler.getClassLoader(), binding);
186     }
187 
188     /**
189      * Binds a new AntBuilder to the project properties.
190      *
191      * @param consoleClass the groovy.ui.Console class to use
192      * @param bindingClass the groovy.lang.Binding class to use
193      * @param console      the groovy.ui.Console object to use
194      * @throws ClassNotFoundException    when a class needed for binding an AntBuilder object cannot be found
195      * @throws IllegalAccessException    when a method needed for binding an AntBuilder object cannot be accessed
196      * @throws InvocationTargetException when a reflection invocation needed for binding an AntBuilder object cannot be completed
197      */
198     protected void bindAntBuilder(Class<?> consoleClass, Class<?> bindingClass, Object console) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException {
199         if (properties.containsKey("ant")) {
200             Class<?> groovyShellClass = classWrangler.getClass("groovy.lang.GroovyShell");
201             Object shell = getField(findField(consoleClass, "shell", groovyShellClass), console);
202             Object binding = invokeMethod(findMethod(groovyShellClass, "getContext"), shell);
203             Object antBuilder = null;
204             try {
205                 antBuilder = invokeConstructor(findConstructor(classWrangler.getClass("groovy.ant.AntBuilder")));
206             } catch (ClassNotFoundException e1) {
207                 getLog().debug("groovy.ant.AntBuilder not available, trying groovy.util.AntBuilder.");
208                 try {
209                     antBuilder = invokeConstructor(findConstructor(classWrangler.getClass("groovy.util.AntBuilder")));
210                 } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException e2) {
211                     logUnableToInitializeAntBuilder(e2);
212                 }
213             } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
214                 logUnableToInitializeAntBuilder(e);
215             }
216             if (antBuilder != null) {
217                 if (bindPropertiesToSeparateVariables) {
218                     invokeMethod(findMethod(bindingClass, "setVariable", String.class, Object.class), binding, "ant", antBuilder);
219                 } else {
220                     properties.put("ant", antBuilder);
221                 }
222             }
223         }
224     }
225 
226     /**
227      * Waits for the console in use to be closed.
228      *
229      * @throws MojoFailureException if the execution was interrupted while running or it was unable to find the console thread to wait on
230      */
231     protected void waitForConsoleClose() throws MojoFailureException {
232         Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
233         Thread[] threadArray = threadSet.toArray(new Thread[0]);
234         Thread consoleThread = null;
235         for (Thread thread : threadArray) {
236             if ("AWT-Shutdown".equals(thread.getName())) {
237                 consoleThread = thread;
238                 break;
239             }
240         }
241         if (consoleThread != null) {
242             try {
243                 consoleThread.join();
244             } catch (InterruptedException e) {
245                 throw new MojoFailureException("Mojo interrupted while waiting for Console thread to end.", e);
246             }
247         } else {
248             throw new MojoFailureException("Unable to locate Console thread to wait on.");
249         }
250     }
251 
252 }