Skip to content

Commit b6c7cb2

Browse files
committed
GROOVY-11871: Support Maven Resolver based version of Grapes
1 parent c2b130b commit b6c7cb2

38 files changed

Lines changed: 1943 additions & 285 deletions

File tree

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ dependencies {
139139

140140
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0'
141141

142-
testRuntimeOnly "org.slf4j:slf4j-simple:${versions.slf4j}"
143142
testRuntimeOnly(project(':')) {
144143
because 'Tests are using Grapes'
145144
capabilities {
146145
requireCapability 'org.apache.groovy:groovy-grapes'
147146
}
148147
}
148+
testRuntimeOnly projects.groovyGrapeIvy
149+
149150
testRuntimeOnly(project(':')) {
150151
because 'Tests are using GPars'
151152
capabilities {

gradle/verification-metadata.xml

Lines changed: 251 additions & 81 deletions
Large diffs are not rendered by default.

settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ def subprojects = [
5656
'groovy-datetime',
5757
'groovy-dateutil',
5858
'groovy-docgenerator',
59+
'groovy-grape-ivy',
60+
'groovy-grape-maven',
61+
'groovy-grape-test',
5962
'groovy-groovydoc',
6063
'groovy-groovysh',
6164
'groovy-jmx',
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.grape
20+
21+
import groovy.transform.CompileStatic
22+
import org.apache.groovy.plugin.GroovyRunner
23+
import org.apache.groovy.plugin.GroovyRunnerRegistry
24+
import org.codehaus.groovy.reflection.CachedClass
25+
import org.codehaus.groovy.reflection.ClassInfo
26+
import org.codehaus.groovy.runtime.m12n.ExtensionModuleScanner
27+
import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl
28+
29+
import java.util.jar.JarFile
30+
import java.util.zip.ZipEntry
31+
import java.util.zip.ZipException
32+
import java.util.zip.ZipFile
33+
34+
/**
35+
* Utility methods shared between GrapeIvy and GrapeMaven implementations.
36+
*/
37+
@CompileStatic
38+
class GrapeUtil {
39+
40+
private static final String METAINF_PREFIX = 'META-INF/services/'
41+
private static final String RUNNER_PROVIDER_CONFIG = GroovyRunner.name
42+
private static final boolean DEBUG_GRAPE = Boolean.getBoolean('groovy.grape.debug')
43+
44+
/**
45+
* Adds a URI to a classloader's classpath via reflection.
46+
*/
47+
static void addURL(ClassLoader loader, URI uri) {
48+
// Dynamic invocation needed as addURL is not part of ClassLoader interface
49+
loader.metaClass.invokeMethod(loader, 'addURL', uri.toURL())
50+
}
51+
52+
/**
53+
* Processes and registers category methods (extension modules) from a JAR file.
54+
*
55+
* @param loader the classloader to register methods with
56+
* @param file the JAR file to process
57+
*/
58+
static void processExtensionMethods(ClassLoader loader, File file) {
59+
// register extension methods if jar
60+
if (file.getName().toLowerCase().endsWith('.jar')) {
61+
def mcRegistry = GroovySystem.metaClassRegistry
62+
if (mcRegistry instanceof MetaClassRegistryImpl) {
63+
try (JarFile jar = new JarFile(file)) {
64+
ZipEntry entry = jar.getEntry(ExtensionModuleScanner.MODULE_META_INF_FILE)
65+
if (!entry) {
66+
entry = jar.getEntry(ExtensionModuleScanner.LEGACY_MODULE_META_INF_FILE)
67+
}
68+
if (entry) {
69+
Properties props = new Properties()
70+
71+
try (InputStream is = jar.getInputStream(entry)) {
72+
props.load(is)
73+
}
74+
75+
Map<CachedClass, List<MetaMethod>> metaMethods = [:]
76+
mcRegistry.registerExtensionModuleFromProperties(props, loader, metaMethods)
77+
// add old methods to the map
78+
metaMethods.each { CachedClass c, List<MetaMethod> methods ->
79+
// GROOVY-5543: if a module was loaded using grab, there are chances that subclasses
80+
// have their own ClassInfo, and we must change them as well!
81+
Set<CachedClass> classesToBeUpdated = [c].toSet()
82+
ClassInfo.onAllClassInfo { ClassInfo info ->
83+
if (c.getTheClass().isAssignableFrom(info.getCachedClass().getTheClass())) {
84+
classesToBeUpdated << info.getCachedClass()
85+
}
86+
}
87+
classesToBeUpdated*.addNewMopMethods(methods)
88+
}
89+
}
90+
} catch (ZipException e) {
91+
throw new RuntimeException("Grape could not load jar '$file'", e)
92+
}
93+
}
94+
}
95+
}
96+
97+
/**
98+
* Searches the given File for known service provider configuration files to process.
99+
*
100+
* @param loader used to locate service provider files
101+
* @param f ZipFile in which to search for services
102+
* @return a collection of service provider files that were found
103+
*/
104+
static Collection<String> processMetaInfServices(ClassLoader loader, File f) {
105+
List<String> services = []
106+
try (ZipFile zf = new ZipFile(f)) {
107+
// TODO: remove in a future release (replaced by GroovyRunnerRegistry)
108+
String providerConfig = 'org.codehaus.groovy.plugins.Runners'
109+
ZipEntry pluginRunners = zf.getEntry(METAINF_PREFIX + providerConfig)
110+
if (pluginRunners != null) {
111+
services.add(providerConfig)
112+
113+
try (InputStream is = zf.getInputStream(pluginRunners)) {
114+
processRunners(is, f.getName(), loader)
115+
}
116+
}
117+
// GroovyRunners are loaded per ClassLoader using a ServiceLoader so here
118+
// it only needs to be indicated that the service provider file was found
119+
if (zf.getEntry(METAINF_PREFIX + RUNNER_PROVIDER_CONFIG) != null) {
120+
services.add(RUNNER_PROVIDER_CONFIG)
121+
}
122+
} catch (ZipException ignore) {
123+
// ignore files we can't process, e.g. non-jar/zip artifacts
124+
if (DEBUG_GRAPE) {
125+
System.err.println "Grape could not process file '$f' for service provider configuration: ${ignore.message}"
126+
}
127+
}
128+
services
129+
}
130+
131+
/**
132+
* Processes and registers Groovy runner implementations from a service provider file.
133+
*
134+
* @param is the input stream containing runner class names
135+
* @param name the name to register the runners under
136+
* @param loader the classloader to load runner classes from
137+
*/
138+
static void processRunners(InputStream is, String name, ClassLoader loader) {
139+
GroovyRunnerRegistry registry = GroovyRunnerRegistry.instance
140+
is.getText().readLines()*.trim().each { String line ->
141+
if (!line.isEmpty() && line[0] != '#') {
142+
try {
143+
registry[name] = (GroovyRunner) loader.loadClass(line).getDeclaredConstructor().newInstance()
144+
} catch (Exception e) {
145+
throw new IllegalStateException("Error registering runner class '$line'", e)
146+
}
147+
}
148+
}
149+
}
150+
151+
static boolean checkForRunner(Collection<String> services) {
152+
services.contains(RUNNER_PROVIDER_CONFIG)
153+
}
154+
155+
static void registryLoad(ClassLoader classLoader) {
156+
GroovyRunnerRegistry.instance.load(classLoader)
157+
}
158+
}

src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
package org.codehaus.groovy.tools
2020

2121
import groovy.grape.Grape
22-
import org.apache.ivy.util.DefaultMessageLogger
23-
import org.apache.ivy.util.Message
2422
import picocli.CommandLine
2523
import picocli.CommandLine.Command
2624
import picocli.CommandLine.Option
@@ -65,12 +63,14 @@ class GrapeMain implements Runnable {
6563
parser.subcommands.findAll { k, v -> k != 'help' }.each { k, v -> v.addMixin('helpOptions', new HelpOptionsMixin()) }
6664

6765
grape.parser = parser
68-
parser.execute(args)
66+
int exitCode = parser.execute(args)
67+
System.exit(exitCode)
6968
}
7069

7170
void run() {
7271
if (unmatched) {
7372
System.err.println "grape: '${unmatched[0]}' is not a grape command. See 'grape --help'"
73+
throw new CommandLine.ParameterException(parser, "Unknown command: ${unmatched[0]}")
7474
} else {
7575
parser.usage(System.out) // if no subcommand was specified
7676
}
@@ -84,20 +84,20 @@ class GrapeMain implements Runnable {
8484
}
8585

8686
@SuppressWarnings('UnusedPrivateMethod') // used in run()
87-
private void setupLogging(int defaultLevel = Message.MSG_INFO) {
87+
private void setupLogging(int defaultLevel = 2) {
88+
int level = defaultLevel
8889
if (quiet) {
89-
Message.defaultLogger = new DefaultMessageLogger(Message.MSG_ERR)
90+
level = 0
9091
} else if (warn) {
91-
Message.defaultLogger = new DefaultMessageLogger(Message.MSG_WARN)
92+
level = 1
9293
} else if (info) {
93-
Message.defaultLogger = new DefaultMessageLogger(Message.MSG_INFO)
94+
level = 2
9495
} else if (verbose) {
95-
Message.defaultLogger = new DefaultMessageLogger(Message.MSG_VERBOSE)
96+
level = 3
9697
} else if (debug) {
97-
Message.defaultLogger = new DefaultMessageLogger(Message.MSG_DEBUG)
98-
} else {
99-
Message.defaultLogger = new DefaultMessageLogger(defaultLevel)
98+
level = 4
10099
}
100+
Grape.instance?.setLoggingLevel(level)
101101
}
102102

103103
/**
@@ -152,10 +152,18 @@ class GrapeMain implements Runnable {
152152
Grape.addResolver(name:url, root:url)
153153
}
154154

155-
try {
156-
Grape.grab(autoDownload: true, group: group, module: module, version: version, classifier: classifier, noExceptions: true)
157-
} catch (Exception ex) {
158-
System.err.println "An error occurred : $ex"
155+
// Call the engine directly to get the exception return value
156+
// The Grape.grab() facade doesn't propagate the return value
157+
def engine = Grape.instance
158+
if (!engine) {
159+
System.err.println "Grape engine not available"
160+
throw new CommandLine.ExecutionException(new CommandLine(this), "Grape engine not initialized")
161+
}
162+
163+
def result = engine.grab(autoDownload: true, group: group, module: module, version: version, classifier: classifier, noExceptions: true)
164+
if (result instanceof Exception) {
165+
System.err.println "Error grabbing Grapes -- ${result.message}"
166+
throw new CommandLine.ExecutionException(new CommandLine(this), "Failed to install grape", result)
159167
}
160168
}
161169
}
@@ -179,7 +187,7 @@ class GrapeMain implements Runnable {
179187
parentCommand.setupLogging()
180188

181189
Grape.enumerateGrapes().each {String groupName, Map group ->
182-
group.each {String moduleName, List<String> versions ->
190+
group.each { String moduleName, List<String> versions ->
183191
println "$groupName $moduleName $versions"
184192
moduleCount++
185193
versionCount += versions.size()
@@ -195,6 +203,7 @@ class GrapeMain implements Runnable {
195203
customSynopsis = 'grape resolve [-adhisv] (<groupId> <artifactId> <version>)+',
196204
description = [
197205
'Prints the file locations of the jars representing the artifacts for the specified module(s) and the respective transitive dependencies.',
206+
'The exact format supported by some parameters depends on the Grape implementation, e.g. Ivy or Maven.',
198207
'',
199208
'Parameters:',
200209
' <group> Which module group the module comes from. Translates directly',
@@ -229,9 +238,9 @@ class GrapeMain implements Runnable {
229238
void run() {
230239
parentCommand.init()
231240

232-
// set the instance so we can re-set the logger
241+
// set the instance so we can re-set the logger (implementation dependent)
233242
Grape.instance
234-
parentCommand.setupLogging(Message.MSG_ERR)
243+
parentCommand.setupLogging(0) // errors only
235244

236245
if ((args.size() % 3) != 0) {
237246
println 'There needs to be a multiple of three arguments: (group module version)+'
@@ -299,6 +308,7 @@ class GrapeMain implements Runnable {
299308
} catch (Exception e) {
300309
System.err.println "Error in resolve:\n\t$e.message"
301310
if (e.message =~ /unresolved dependency/) println 'Perhaps the grape is not installed?'
311+
throw new CommandLine.ExecutionException(new CommandLine(this), "Failed to resolve grape", e)
302312
}
303313
}
304314
}

src/main/java/groovy/grape/GrabAnnotationTransformation.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.codehaus.groovy.ast.ClassNode;
3333
import org.codehaus.groovy.ast.ImportNode;
3434
import org.codehaus.groovy.ast.ModuleNode;
35+
import org.codehaus.groovy.ast.expr.ClassExpression;
3536
import org.codehaus.groovy.ast.expr.ConstantExpression;
3637
import org.codehaus.groovy.ast.expr.Expression;
3738
import org.codehaus.groovy.ast.expr.ListExpression;
@@ -392,6 +393,11 @@ private void callGrabAsStaticInitIfNeeded(final ClassNode classNode, final Class
392393
final Collection<Map<String,Object>> grabMapsInit, final Collection<Map<String, Object>> grabExcludeMaps) {
393394
List<Statement> grabInitializers = new ArrayList<>();
394395
MapExpression basicArgs = new MapExpression();
396+
// Pass the class's own ClassLoader so chooseClassLoader doesn't have to walk the
397+
// call stack — the stack-walk depth is tuned for the compile-time path, not the
398+
// generated static-initializer path, and would overshoot into java.lang.reflect frames.
399+
basicArgs.addMapEntryExpression(constX("classLoader"),
400+
callX(new ClassExpression(classNode), "getClassLoader"));
395401
if (autoDownload != null) {
396402
basicArgs.addMapEntryExpression(constX(AUTO_DOWNLOAD_SETTING), constX(autoDownload));
397403
}

0 commit comments

Comments
 (0)