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 }