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