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 }