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              getLog().error("Your Groovy version (" + classWrangler.getGroovyVersionString() + ") doesn't support running a console. The minimum version of Groovy required is " + minGroovyVersion + ". Skipping console startup.");
90              return;
91          }
92  
93          final SecurityManager defaultSecurityManager = System.getSecurityManager();
94          try {
95              if (!allowSystemExits) {
96                  getLog().warn("JEP 411 deprecated Security Manager in Java 17 for removal. Therefore `allowSystemExits` is also deprecated for removal.");
97                  try {
98                      System.setSecurityManager(new NoExitSecurityManager());
99                  } catch (UnsupportedOperationException e) {
100                     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.");
101                 }
102             }
103 
104             // get classes we need with reflection
105             Class<?> consoleClass;
106             try {
107                 consoleClass = classWrangler.getClass("groovy.console.ui.Console");
108             } catch (ClassNotFoundException e) {
109                 consoleClass = classWrangler.getClass("groovy.ui.Console");
110             }
111             Class<?> bindingClass = classWrangler.getClass("groovy.lang.Binding");
112 
113             // create console to run
114             Object console = setupConsole(consoleClass, bindingClass);
115 
116             // run the console
117             invokeMethod(findMethod(consoleClass, "run"), console);
118 
119             // TODO: for some reason instantiating AntBuilder before calling run() causes its stdout and stderr streams to not be captured by the Console
120             bindAntBuilder(consoleClass, bindingClass, console);
121 
122             // open script file
123             loadScript(consoleClass, console);
124 
125             // wait for console to be closed
126             waitForConsoleClose();
127         } catch (ClassNotFoundException e) {
128             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);
129         } catch (InvocationTargetException e) {
130             if (e.getCause() instanceof NoClassDefFoundError && "org/apache/ivy/core/report/ResolveReport".equals(e.getCause().getMessage())) {
131                 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);
132             } else {
133                 throw new MojoExecutionException("Error occurred while calling a method on a Groovy class from classpath.", e);
134             }
135         } catch (IllegalAccessException e) {
136             throw new MojoExecutionException("Unable to access a method on a Groovy class from classpath.", e);
137         } catch (InstantiationException e) {
138             throw new MojoExecutionException("Error occurred while instantiating a Groovy class from classpath.", e);
139         } finally {
140             if (!allowSystemExits) {
141                 try {
142                     System.setSecurityManager(defaultSecurityManager);
143                 } catch (UnsupportedOperationException e) {
144                     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.");
145                 }
146             }
147         }
148     }
149 
150     protected void loadScript(Class<?> consoleClass, Object console) throws InvocationTargetException, IllegalAccessException {
151         if (consoleScript != null) {
152             Method loadScriptFile = findMethod(consoleClass, "loadScriptFile", File.class);
153             File consoleScriptFile = new File(consoleScript);
154             if (consoleScriptFile.isFile()) {
155                 invokeMethod(loadScriptFile, console, consoleScriptFile);
156             } else if (project.getProperties().containsKey(consoleScript)) {
157                 consoleScriptFile = new File(project.getProperties().getProperty(consoleScript));
158                 if (consoleScriptFile.isFile()) {
159                     invokeMethod(loadScriptFile, console, consoleScriptFile);
160                 } else {
161                     getLog().warn("consoleScript ('" + consoleScript + "') doesn't exist in project properties or as a file.");
162                 }
163             } else {
164                 getLog().warn("consoleScript ('" + consoleScript + "') doesn't exist in project properties or as a file.");
165             }
166         }
167     }
168 
169     /**
170      * Instantiates a groovy.ui.Console object.
171      *
172      * @param consoleClass the groovy.ui.Console class to use
173      * @param bindingClass the groovy.lang.Binding class to use
174      * @return a new groovy.ui.Console object
175      * @throws InvocationTargetException when a reflection invocation needed for instantiating a console object cannot be completed
176      * @throws IllegalAccessException    when a method needed for instantiating a console object cannot be accessed
177      * @throws InstantiationException    when a class needed for instantiating a console object cannot be instantiated
178      */
179     protected Object setupConsole(final Class<?> consoleClass, final Class<?> bindingClass) throws InvocationTargetException, IllegalAccessException, InstantiationException {
180         Object binding = invokeConstructor(findConstructor(bindingClass));
181         initializeProperties();
182         Method setVariable = findMethod(bindingClass, "setVariable", String.class, Object.class);
183         if (bindPropertiesToSeparateVariables) {
184             for (Object k : properties.keySet()) {
185                 invokeMethod(setVariable, binding, k, properties.get(k));
186             }
187         } else {
188             if (groovyOlderThan(GROOVY_4_0_0_RC_1)) {
189                 invokeMethod(setVariable, binding, "properties", properties);
190             } else {
191                 throw new IllegalArgumentException("properties is a read-only property in Groovy " + GROOVY_4_0_0_RC_1 + " and later.");
192             }
193         }
194 
195         return invokeConstructor(findConstructor(consoleClass, ClassLoader.class, bindingClass), classWrangler.getClassLoader(), binding);
196     }
197 
198     /**
199      * Binds a new AntBuilder to the project properties.
200      *
201      * @param consoleClass the groovy.ui.Console class to use
202      * @param bindingClass the groovy.lang.Binding class to use
203      * @param console      the groovy.ui.Console object to use
204      * @throws ClassNotFoundException    when a class needed for binding an AntBuilder object cannot be found
205      * @throws IllegalAccessException    when a method needed for binding an AntBuilder object cannot be accessed
206      * @throws InvocationTargetException when a reflection invocation needed for binding an AntBuilder object cannot be completed
207      */
208     protected void bindAntBuilder(Class<?> consoleClass, Class<?> bindingClass, Object console) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException {
209         if (properties.containsKey("ant")) {
210             Class<?> groovyShellClass = classWrangler.getClass("groovy.lang.GroovyShell");
211             Object shell = getField(findField(consoleClass, "shell", groovyShellClass), console);
212             Object binding = invokeMethod(findMethod(groovyShellClass, "getContext"), shell);
213             Object antBuilder = null;
214             try {
215                 antBuilder = invokeConstructor(findConstructor(classWrangler.getClass("groovy.ant.AntBuilder")));
216             } catch (ClassNotFoundException e1) {
217                 getLog().debug("groovy.ant.AntBuilder not available, trying groovy.util.AntBuilder.");
218                 try {
219                     antBuilder = invokeConstructor(findConstructor(classWrangler.getClass("groovy.util.AntBuilder")));
220                 } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException e2) {
221                     logUnableToInitializeAntBuilder(e2);
222                 }
223             } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
224                 logUnableToInitializeAntBuilder(e);
225             }
226             if (antBuilder != null) {
227                 if (bindPropertiesToSeparateVariables) {
228                     invokeMethod(findMethod(bindingClass, "setVariable", String.class, Object.class), binding, "ant", antBuilder);
229                 } else {
230                     properties.put("ant", antBuilder);
231                 }
232             }
233         }
234     }
235 
236     /**
237      * Waits for the console in use to be closed.
238      *
239      * @throws MojoFailureException if the execution was interrupted while running or it was unable to find the console thread to wait on
240      */
241     protected void waitForConsoleClose() throws MojoFailureException {
242         Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
243         Thread[] threadArray = threadSet.toArray(new Thread[0]);
244         Thread consoleThread = null;
245         for (Thread thread : threadArray) {
246             if ("AWT-Shutdown".equals(thread.getName())) {
247                 consoleThread = thread;
248                 break;
249             }
250         }
251         if (consoleThread != null) {
252             try {
253                 consoleThread.join();
254             } catch (InterruptedException e) {
255                 throw new MojoFailureException("Mojo interrupted while waiting for Console thread to end.", e);
256             }
257         } else {
258             throw new MojoFailureException("Unable to locate Console thread to wait on.");
259         }
260     }
261 
262 }