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