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