View Javadoc
1   package org.codehaus.gmavenplus.util;
2   
3   import org.apache.maven.plugin.logging.Log;
4   import org.codehaus.gmavenplus.model.internal.Version;
5   
6   import java.io.File;
7   import java.lang.reflect.InvocationTargetException;
8   import java.net.MalformedURLException;
9   import java.net.URL;
10  import java.net.URLClassLoader;
11  import java.security.CodeSource;
12  import java.util.ArrayList;
13  import java.util.List;
14  
15  import static org.codehaus.gmavenplus.util.ReflectionUtils.findMethod;
16  import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeStaticMethod;
17  
18  
19  /**
20   * Handles getting Groovy classes and version from the specified classpath.
21   *
22   * @author Keegan Witt
23   */
24  public class ClassWrangler {
25  
26      /**
27       * Cached Groovy version.
28       */
29      private String groovyVersion = null;
30  
31      /**
32       * Cached whether Groovy supports invokedynamic (indy jar).
33       */
34      private Boolean isIndy = null;
35  
36      /**
37       * ClassLoader to use for class wrangling.
38       */
39      private final ClassLoader classLoader;
40  
41      /**
42       * Plugin log.
43       */
44      private final Log log;
45  
46      /**
47       * Creates a new ClassWrangler using the specified parent ClassLoader, loaded with the items from the specified classpath.
48       *
49       * @param classpath         the classpath to load the new ClassLoader with
50       * @param parentClassLoader the parent for the new ClassLoader used to use to load classes
51       * @param pluginLog         the Maven log to use for logging
52       * @throws MalformedURLException when a classpath element provides a malformed URL
53       */
54      public ClassWrangler(final List<?> classpath, final ClassLoader parentClassLoader, final Log pluginLog) throws MalformedURLException {
55          log = pluginLog;
56          classLoader = createNewClassLoader(classpath, parentClassLoader);
57          Thread.currentThread().setContextClassLoader(classLoader);
58      }
59  
60      /**
61       * Gets the version string of Groovy used from classpath.
62       *
63       * @return The version string of Groovy used by the project
64       */
65      public String getGroovyVersionString() {
66          if (groovyVersion == null) {
67              // this method should work for all Groovy versions >= 1.6.6
68              try {
69                  Class<?> groovySystemClass = getClass("groovy.lang.GroovySystem");
70                  String ver = (String) invokeStaticMethod(findMethod(groovySystemClass, "getVersion"));
71                  if (ver != null && !ver.isEmpty()) {
72                      groovyVersion = ver;
73                  }
74              } catch (ClassNotFoundException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
75                  // do nothing, will try another way
76              }
77  
78              // this should work for Groovy versions < 1.6.6 (technically can work up to 1.9.0)
79              if (groovyVersion == null) {
80                  log.info("Unable to get Groovy version from GroovySystem, trying InvokerHelper.");
81                  try {
82                      Class<?> invokerHelperClass = getClass("org.codehaus.groovy.runtime.InvokerHelper");
83                      String ver = (String) invokeStaticMethod(findMethod(invokerHelperClass, "getVersion"));
84                      if (ver != null && !ver.isEmpty()) {
85                          groovyVersion = ver;
86                      }
87                  } catch (ClassNotFoundException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
88                      // do nothing, will try another way
89                  }
90              }
91  
92              /*
93               * This handles the circumstances in which neither the GroovySystem or InvokerHelper methods
94               * worked (GAE with versions older than 1.6.6 is one example, see
95               * https://jira.codehaus.org/browse/GROOVY-3884). One case this can't handle properly is uber
96               * jars that include Groovy. It should also be noted this method assumes jars will be named
97               * in the Maven convention (<artifactId>-<version>-<classifier>.jar).
98               */
99              if (groovyVersion == null) {
100                 log.warn("Unable to get Groovy version from InvokerHelper or GroovySystem, trying jar name.");
101                 String jar = getGroovyJar();
102                 int idx = Integer.MAX_VALUE;
103                 for (int i = 0; i < 9; i++) {
104                     int newIdx = jar.indexOf("-" + i);
105                     if (newIdx >= 0 && newIdx < idx) {
106                         idx = newIdx;
107                     }
108                 }
109                 if (idx < Integer.MAX_VALUE) {
110                     groovyVersion = jar.substring(idx + 1, jar.length() - 4).replace("-indy", "").replace("-grooid", "");
111                 }
112             }
113         }
114 
115         return groovyVersion;
116     }
117 
118     /**
119      * Gets the version of Groovy used from the classpath.
120      *
121      * @return The version of Groovy used by the project
122      */
123     public Version getGroovyVersion() {
124         try {
125             return Version.parseFromString(getGroovyVersionString());
126         } catch (Exception e) {
127             throw new RuntimeException("Unable to determine Groovy version. Is Groovy declared as a dependency?");
128         }
129     }
130 
131     /**
132      * Determines whether the detected Groovy version is the specified version or newer.
133      *
134      * @param detectedVersion  the detected Groovy version
135      * @param compareToVersion the version to compare the detected Groovy version to
136      * @return <code>true</code> if the detected Groovy version is the specified version or newer, <code>false</code> otherwise
137      */
138     public static boolean groovyAtLeast(Version detectedVersion, Version compareToVersion) {
139         return detectedVersion.compareTo(compareToVersion) >= 0;
140     }
141 
142     /**
143      * Determines whether the detected Groovy version is the specified version.
144      *
145      * @param detectedVersion  the detected Groovy version
146      * @param compareToVersion the version to compare the detected Groovy version to
147      * @return <code>true</code> if the detected Groovy version is the specified version, <code>false</code> otherwise
148      */
149     public static boolean groovyIs(Version detectedVersion, Version compareToVersion) {
150         return detectedVersion.compareTo(compareToVersion) == 0;
151     }
152 
153     /**
154      * Determines whether the detected Groovy version is newer than the specified version.
155      *
156      * @param detectedVersion  the detected Groovy version
157      * @param compareToVersion the version to compare the detected Groovy version to
158      * @return <code>true</code> if the detected Groovy version is newer than the specified version, <code>false</code> otherwise
159      */
160     public static boolean groovyNewerThan(Version detectedVersion, Version compareToVersion) {
161         return detectedVersion.compareTo(compareToVersion) > 0;
162     }
163 
164     /**
165      * Determines whether the detected Groovy version is older than the specified version.
166      *
167      * @param detectedVersion  the detected Groovy version
168      * @param compareToVersion the version to compare the detected Groovy version to
169      * @return <code>true</code> if the detected Groovy version is older than the specified version, <code>false</code> otherwise
170      */
171     public static boolean groovyOlderThan(Version detectedVersion, Version compareToVersion) {
172         return detectedVersion.compareTo(compareToVersion) < 0;
173     }
174 
175     /**
176      * Gets whether the version of Groovy on the classpath supports invokedynamic.
177      *
178      * @return <code>true</code> if the version of Groovy uses invokedynamic,
179      * <code>false</code> if not or Groovy dependency cannot be found.
180      */
181     public boolean isGroovyIndy() {
182         if (isIndy == null) {
183             try {
184                 getClass("org.codehaus.groovy.vmplugin.v8.IndyInterface");
185                 isIndy = true;
186             } catch (ClassNotFoundException e1) {
187                 try {
188                     getClass("org.codehaus.groovy.vmplugin.v7.IndyInterface");
189                     isIndy = true;
190                 } catch (ClassNotFoundException e2) {
191                     isIndy = false;
192                 }
193             }
194         }
195 
196         return isIndy;
197     }
198 
199     /**
200      * Logs the version of groovy used by this mojo.
201      *
202      * @param goal The goal to mention in the log statement showing Groovy version
203      */
204     public void logGroovyVersion(final String goal) {
205         log.info("Using Groovy " + getGroovyVersionString() + " to perform " + goal + ".");
206     }
207 
208     /**
209      * Gets a class for the given class name.
210      *
211      * @param className the class name to retrieve the class for
212      * @return the class for the given class name
213      * @throws ClassNotFoundException when a class for the specified class name cannot be found
214      */
215     public Class<?> getClass(final String className) throws ClassNotFoundException {
216         return Class.forName(className, true, classLoader);
217     }
218 
219     /**
220      * Returns the classloader used for loading classes.
221      *
222      * @return the classloader used for loading classes
223      */
224     public ClassLoader getClassLoader() {
225         return classLoader;
226     }
227 
228     /**
229      * Creates a new ClassLoader with the specified classpath.
230      *
231      * @param classpath   the classpath (a list of file path Strings) to include in the new loader
232      * @param classLoader the ClassLoader to use as the parent for the new CLassLoader
233      * @return the new ClassLoader
234      * @throws MalformedURLException when a classpath element provides a malformed URL
235      */
236     protected ClassLoader createNewClassLoader(final List<?> classpath, final ClassLoader classLoader) throws MalformedURLException {
237         List<URL> urlsList = new ArrayList<>();
238         for (Object classPathObject : classpath) {
239             String path = (String) classPathObject;
240             urlsList.add(new File(path).toURI().toURL());
241         }
242         URL[] urlsArray = urlsList.toArray(new URL[0]);
243         return new URLClassLoader(urlsArray, classLoader);
244     }
245 
246     /**
247      * Returns the filename of the Groovy jar on the classpath.
248      *
249      * @return the Groovy jar filename
250      */
251     protected String getGroovyJar() {
252         try {
253             String groovyObjectClassPath = getJarPath();
254             String groovyJar = null;
255             if (groovyObjectClassPath != null) {
256                 groovyJar = groovyObjectClassPath.replaceAll("!.+", "");
257                 groovyJar = groovyJar.substring(groovyJar.lastIndexOf("/") + 1);
258             }
259 
260             return groovyJar;
261         } catch (ClassNotFoundException e) {
262             throw new RuntimeException("Unable to determine Groovy version. Is Groovy declared as a dependency?");
263         }
264     }
265 
266     /**
267      * Returns the path of the Groovy jar on the classpath.
268      *
269      * @return the path of the Groovy jar
270      * @throws ClassNotFoundException when Groovy couldn't be found on the classpath
271      */
272     protected String getJarPath() throws ClassNotFoundException {
273         Class<?> groovyObjectClass = getClass("groovy.lang.GroovyObject");
274         String groovyObjectClassPath = String.valueOf(groovyObjectClass.getResource("/" + groovyObjectClass.getName().replace('.', '/') + ".class"));
275         if (groovyObjectClassPath == null) {
276             CodeSource codeSource = groovyObjectClass.getProtectionDomain().getCodeSource();
277             if (codeSource != null) {
278                 groovyObjectClassPath = String.valueOf(codeSource.getLocation());
279             }
280         }
281         return groovyObjectClassPath;
282     }
283 }