From bb8613147bc68a5f0a45698d6ee4f35225ee01ec Mon Sep 17 00:00:00 2001 From: Paul King Date: Mon, 13 Jan 2025 23:05:54 +1000 Subject: [PATCH] GROOVY-11871: Support Maven Resolver based version of Grapes --- build.gradle | 3 +- gradle/verification-metadata.xml | 332 ++++++-- settings.gradle | 3 + src/main/groovy/groovy/grape/GrapeUtil.groovy | 158 ++++ .../codehaus/groovy/tools/GrapeMain.groovy | 46 +- .../grape/GrabAnnotationTransformation.java | 6 + src/main/java/groovy/grape/Grape.java | 121 ++- src/main/java/groovy/grape/GrapeEngine.java | 6 + src/spec/test/DesignPatternsTest.groovy | 6 +- src/test/groovy/bugs/Groovy10461.groovy | 1 + .../grape/GrabErrorIsolationTest.groovy | 3 +- .../groovy/grape/GrabResolverTest.groovy | 14 +- .../m12n/ExtensionModuleHelperForTests.groovy | 3 +- .../traitx/TraitASTTransformationTest.groovy | 1 + subprojects/groovy-console/build.gradle | 1 + .../groovy/groovy/console/ui/Console.groovy | 16 +- .../groovy/console/ui/ConsoleIvyPlugin.groovy | 48 +- .../console/ui/ConsoleMavenPlugin.groovy | 53 ++ subprojects/groovy-grape-ivy/build.gradle | 29 + .../groovy/groovy/grape/ivy}/GrapeIvy.groovy | 188 ++--- .../services/groovy.grape.GrapeEngine | 15 + .../groovy/grape/ivy}/defaultGrapeConfig.xml | 0 .../groovy/grape/ivy}/GrapeIvyTest.groovy | 59 +- subprojects/groovy-grape-maven/build.gradle | 28 + .../groovy/grape/maven/GrapeMaven.groovy | 770 ++++++++++++++++++ .../services/groovy.grape.GrapeEngine | 15 + .../groovy/grape/maven/GrapeMavenTest.groovy | 150 ++++ subprojects/groovy-grape-test/build.gradle | 39 + .../GrapeConfiguredMavenSelectionTest.groovy | 37 + .../GrapeDefaultProviderSelectionTest.groovy | 34 + .../GrapeImplementationSelectionTest.groovy | 36 + .../grape/GrapeSelectionTestSupport.groovy | 36 + subprojects/groovy-jmx/build.gradle | 1 + subprojects/groovy-json/build.gradle | 1 + subprojects/groovy-sql/build.gradle | 1 + subprojects/groovy-xml/build.gradle | 1 + .../main/groovy/groovy/xml/StaxBuilder.groovy | 5 +- .../src/spec/test/StaxBuilderTest.groovy | 6 +- versions.properties | 4 +- 39 files changed, 1968 insertions(+), 308 deletions(-) create mode 100644 src/main/groovy/groovy/grape/GrapeUtil.groovy create mode 100644 subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleMavenPlugin.groovy create mode 100644 subprojects/groovy-grape-ivy/build.gradle rename {src/main/groovy/groovy/grape => subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy}/GrapeIvy.groovy (80%) create mode 100644 subprojects/groovy-grape-ivy/src/main/resources/META-INF/services/groovy.grape.GrapeEngine rename {src/resources/groovy/grape => subprojects/groovy-grape-ivy/src/main/resources/groovy/grape/ivy}/defaultGrapeConfig.xml (100%) rename {src/test/groovy/groovy/grape => subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy}/GrapeIvyTest.groovy (89%) create mode 100644 subprojects/groovy-grape-maven/build.gradle create mode 100644 subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy create mode 100644 subprojects/groovy-grape-maven/src/main/resources/META-INF/services/groovy.grape.GrapeEngine create mode 100644 subprojects/groovy-grape-maven/src/test/groovy/groovy/grape/maven/GrapeMavenTest.groovy create mode 100644 subprojects/groovy-grape-test/build.gradle create mode 100644 subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeConfiguredMavenSelectionTest.groovy create mode 100644 subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeDefaultProviderSelectionTest.groovy create mode 100644 subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeImplementationSelectionTest.groovy create mode 100644 subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeSelectionTestSupport.groovy diff --git a/build.gradle b/build.gradle index b0541b518bc..5697f82df7c 100644 --- a/build.gradle +++ b/build.gradle @@ -143,13 +143,14 @@ dependencies { spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0' - testRuntimeOnly "org.slf4j:slf4j-simple:${versions.slf4j}" testRuntimeOnly(project(':')) { because 'Tests are using Grapes' capabilities { requireCapability 'org.apache.groovy:groovy-grapes' } } + testRuntimeOnly projects.groovyGrapeIvy + testRuntimeOnly(project(':')) { because 'Tests are using GPars' capabilities { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5c952c2e108..145bbe0ebb3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -130,7 +130,10 @@ - + + + + @@ -346,29 +349,34 @@ - - - + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -779,6 +787,7 @@ + @@ -829,8 +838,8 @@ - - + + @@ -1205,7 +1214,7 @@ - + @@ -1215,7 +1224,7 @@ - + @@ -1265,9 +1274,64 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1275,29 +1339,44 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + + + + + + @@ -1315,24 +1394,49 @@ - - - + + + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + + + + + + + + + + + @@ -1366,29 +1470,74 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + @@ -1527,6 +1676,11 @@ + + + + + @@ -1601,6 +1755,16 @@ + + + + + + + + + + @@ -1612,6 +1776,11 @@ + + + + + @@ -1628,6 +1797,16 @@ + + + + + + + + + + @@ -1673,24 +1852,14 @@ - - - - - - - - - - - - - + + + - - - + + + @@ -2372,6 +2541,7 @@ + diff --git a/settings.gradle b/settings.gradle index 5f3364d56af..9600e9d5de4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,6 +56,9 @@ def subprojects = [ 'groovy-datetime', 'groovy-dateutil', 'groovy-docgenerator', + 'groovy-grape-ivy', + 'groovy-grape-maven', + 'groovy-grape-test', 'groovy-groovydoc', 'groovy-groovysh', 'groovy-jmx', diff --git a/src/main/groovy/groovy/grape/GrapeUtil.groovy b/src/main/groovy/groovy/grape/GrapeUtil.groovy new file mode 100644 index 00000000000..6bf1687e5a1 --- /dev/null +++ b/src/main/groovy/groovy/grape/GrapeUtil.groovy @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape + +import groovy.transform.CompileStatic +import org.apache.groovy.plugin.GroovyRunner +import org.apache.groovy.plugin.GroovyRunnerRegistry +import org.codehaus.groovy.reflection.CachedClass +import org.codehaus.groovy.reflection.ClassInfo +import org.codehaus.groovy.runtime.m12n.ExtensionModuleScanner +import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl + +import java.util.jar.JarFile +import java.util.zip.ZipEntry +import java.util.zip.ZipException +import java.util.zip.ZipFile + +/** + * Utility methods shared between GrapeIvy and GrapeMaven implementations. + */ +@CompileStatic +class GrapeUtil { + + private static final String METAINF_PREFIX = 'META-INF/services/' + private static final String RUNNER_PROVIDER_CONFIG = GroovyRunner.name + private static final boolean DEBUG_GRAPE = Boolean.getBoolean('groovy.grape.debug') + + /** + * Adds a URI to a classloader's classpath via reflection. + */ + static void addURL(ClassLoader loader, URI uri) { + // Dynamic invocation needed as addURL is not part of ClassLoader interface + loader.metaClass.invokeMethod(loader, 'addURL', uri.toURL()) + } + + /** + * Processes and registers category methods (extension modules) from a JAR file. + * + * @param loader the classloader to register methods with + * @param file the JAR file to process + */ + static void processExtensionMethods(ClassLoader loader, File file) { + // register extension methods if jar + if (file.getName().toLowerCase().endsWith('.jar')) { + def mcRegistry = GroovySystem.metaClassRegistry + if (mcRegistry instanceof MetaClassRegistryImpl) { + try (JarFile jar = new JarFile(file)) { + ZipEntry entry = jar.getEntry(ExtensionModuleScanner.MODULE_META_INF_FILE) + if (!entry) { + entry = jar.getEntry(ExtensionModuleScanner.LEGACY_MODULE_META_INF_FILE) + } + if (entry) { + Properties props = new Properties() + + try (InputStream is = jar.getInputStream(entry)) { + props.load(is) + } + + Map> metaMethods = [:] + mcRegistry.registerExtensionModuleFromProperties(props, loader, metaMethods) + // add old methods to the map + metaMethods.each { CachedClass c, List methods -> + // GROOVY-5543: if a module was loaded using grab, there are chances that subclasses + // have their own ClassInfo, and we must change them as well! + Set classesToBeUpdated = [c].toSet() + ClassInfo.onAllClassInfo { ClassInfo info -> + if (c.getTheClass().isAssignableFrom(info.getCachedClass().getTheClass())) { + classesToBeUpdated << info.getCachedClass() + } + } + classesToBeUpdated*.addNewMopMethods(methods) + } + } + } catch (ZipException e) { + throw new RuntimeException("Grape could not load jar '$file'", e) + } + } + } + } + + /** + * Searches the given File for known service provider configuration files to process. + * + * @param loader used to locate service provider files + * @param f ZipFile in which to search for services + * @return a collection of service provider files that were found + */ + static Collection processMetaInfServices(ClassLoader loader, File f) { + List services = [] + try (ZipFile zf = new ZipFile(f)) { + // TODO: remove in a future release (replaced by GroovyRunnerRegistry) + String providerConfig = 'org.codehaus.groovy.plugins.Runners' + ZipEntry pluginRunners = zf.getEntry(METAINF_PREFIX + providerConfig) + if (pluginRunners != null) { + services.add(providerConfig) + + try (InputStream is = zf.getInputStream(pluginRunners)) { + processRunners(is, f.getName(), loader) + } + } + // GroovyRunners are loaded per ClassLoader using a ServiceLoader so here + // it only needs to be indicated that the service provider file was found + if (zf.getEntry(METAINF_PREFIX + RUNNER_PROVIDER_CONFIG) != null) { + services.add(RUNNER_PROVIDER_CONFIG) + } + } catch (ZipException ignore) { + // ignore files we can't process, e.g. non-jar/zip artifacts + if (DEBUG_GRAPE) { + System.err.println "Grape could not process file '$f' for service provider configuration: ${ignore.message}" + } + } + services + } + + /** + * Processes and registers Groovy runner implementations from a service provider file. + * + * @param is the input stream containing runner class names + * @param name the name to register the runners under + * @param loader the classloader to load runner classes from + */ + static void processRunners(InputStream is, String name, ClassLoader loader) { + GroovyRunnerRegistry registry = GroovyRunnerRegistry.instance + is.getText().readLines()*.trim().each { String line -> + if (!line.isEmpty() && line[0] != '#') { + try { + registry[name] = (GroovyRunner) loader.loadClass(line).getDeclaredConstructor().newInstance() + } catch (Exception e) { + throw new IllegalStateException("Error registering runner class '$line'", e) + } + } + } + } + + static boolean checkForRunner(Collection services) { + services.contains(RUNNER_PROVIDER_CONFIG) + } + + static void registryLoad(ClassLoader classLoader) { + GroovyRunnerRegistry.instance.load(classLoader) + } +} diff --git a/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy b/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy index e3eba5682d3..d1ec470f909 100644 --- a/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy +++ b/src/main/groovy/org/codehaus/groovy/tools/GrapeMain.groovy @@ -19,8 +19,6 @@ package org.codehaus.groovy.tools import groovy.grape.Grape -import org.apache.ivy.util.DefaultMessageLogger -import org.apache.ivy.util.Message import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.Option @@ -65,12 +63,14 @@ class GrapeMain implements Runnable { parser.subcommands.findAll { k, v -> k != 'help' }.each { k, v -> v.addMixin('helpOptions', new HelpOptionsMixin()) } grape.parser = parser - parser.execute(args) + int exitCode = parser.execute(args) + System.exit(exitCode) } void run() { if (unmatched) { System.err.println "grape: '${unmatched[0]}' is not a grape command. See 'grape --help'" + throw new CommandLine.ParameterException(parser, "Unknown command: ${unmatched[0]}") } else { parser.usage(System.out) // if no subcommand was specified } @@ -84,20 +84,20 @@ class GrapeMain implements Runnable { } @SuppressWarnings('UnusedPrivateMethod') // used in run() - private void setupLogging(int defaultLevel = Message.MSG_INFO) { + private void setupLogging(int defaultLevel = 2) { + int level = defaultLevel if (quiet) { - Message.defaultLogger = new DefaultMessageLogger(Message.MSG_ERR) + level = 0 } else if (warn) { - Message.defaultLogger = new DefaultMessageLogger(Message.MSG_WARN) + level = 1 } else if (info) { - Message.defaultLogger = new DefaultMessageLogger(Message.MSG_INFO) + level = 2 } else if (verbose) { - Message.defaultLogger = new DefaultMessageLogger(Message.MSG_VERBOSE) + level = 3 } else if (debug) { - Message.defaultLogger = new DefaultMessageLogger(Message.MSG_DEBUG) - } else { - Message.defaultLogger = new DefaultMessageLogger(defaultLevel) + level = 4 } + Grape.instance?.setLoggingLevel(level) } /** @@ -152,10 +152,18 @@ class GrapeMain implements Runnable { Grape.addResolver(name:url, root:url) } - try { - Grape.grab(autoDownload: true, group: group, module: module, version: version, classifier: classifier, noExceptions: true) - } catch (Exception ex) { - System.err.println "An error occurred : $ex" + // Call the engine directly to get the exception return value + // The Grape.grab() facade doesn't propagate the return value + def engine = Grape.instance + if (!engine) { + System.err.println "Grape engine not available" + throw new CommandLine.ExecutionException(new CommandLine(this), "Grape engine not initialized") + } + + def result = engine.grab(autoDownload: true, group: group, module: module, version: version, classifier: classifier, noExceptions: true) + if (result instanceof Exception) { + System.err.println "Error grabbing Grapes -- ${result.message}" + throw new CommandLine.ExecutionException(new CommandLine(this), "Failed to install grape", result) } } } @@ -179,7 +187,7 @@ class GrapeMain implements Runnable { parentCommand.setupLogging() Grape.enumerateGrapes().each {String groupName, Map group -> - group.each {String moduleName, List versions -> + group.each { String moduleName, List versions -> println "$groupName $moduleName $versions" moduleCount++ versionCount += versions.size() @@ -195,6 +203,7 @@ class GrapeMain implements Runnable { customSynopsis = 'grape resolve [-adhisv] ( )+', description = [ 'Prints the file locations of the jars representing the artifacts for the specified module(s) and the respective transitive dependencies.', + 'The exact format supported by some parameters depends on the Grape implementation, e.g. Ivy or Maven.', '', 'Parameters:', ' Which module group the module comes from. Translates directly', @@ -229,9 +238,9 @@ class GrapeMain implements Runnable { void run() { parentCommand.init() - // set the instance so we can re-set the logger + // set the instance so we can re-set the logger (implementation dependent) Grape.instance - parentCommand.setupLogging(Message.MSG_ERR) + parentCommand.setupLogging(0) // errors only if ((args.size() % 3) != 0) { println 'There needs to be a multiple of three arguments: (group module version)+' @@ -299,6 +308,7 @@ class GrapeMain implements Runnable { } catch (Exception e) { System.err.println "Error in resolve:\n\t$e.message" if (e.message =~ /unresolved dependency/) println 'Perhaps the grape is not installed?' + throw new CommandLine.ExecutionException(new CommandLine(this), "Failed to resolve grape", e) } } } diff --git a/src/main/java/groovy/grape/GrabAnnotationTransformation.java b/src/main/java/groovy/grape/GrabAnnotationTransformation.java index 05e8a717baa..c55a0e82f31 100644 --- a/src/main/java/groovy/grape/GrabAnnotationTransformation.java +++ b/src/main/java/groovy/grape/GrabAnnotationTransformation.java @@ -32,6 +32,7 @@ import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.expr.ClassExpression; import org.codehaus.groovy.ast.expr.ConstantExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.ListExpression; @@ -392,6 +393,11 @@ private void callGrabAsStaticInitIfNeeded(final ClassNode classNode, final Class final Collection> grabMapsInit, final Collection> grabExcludeMaps) { List grabInitializers = new ArrayList<>(); MapExpression basicArgs = new MapExpression(); + // Pass the class's own ClassLoader so chooseClassLoader doesn't have to walk the + // call stack — the stack-walk depth is tuned for the compile-time path, not the + // generated static-initializer path, and would overshoot into java.lang.reflect frames. + basicArgs.addMapEntryExpression(constX("classLoader"), + callX(new ClassExpression(classNode), "getClassLoader")); if (autoDownload != null) { basicArgs.addMapEntryExpression(constX(AUTO_DOWNLOAD_SETTING), constX(autoDownload)); } diff --git a/src/main/java/groovy/grape/Grape.java b/src/main/java/groovy/grape/Grape.java index e392f2b06ef..650762e0efb 100644 --- a/src/main/java/groovy/grape/Grape.java +++ b/src/main/java/groovy/grape/Grape.java @@ -19,10 +19,12 @@ package groovy.grape; import java.net.URI; -import java.security.PrivilegedAction; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; /** * Facade to GrapeEngine. @@ -32,6 +34,8 @@ public class Grape { public static final String AUTO_DOWNLOAD_SETTING = "autoDownload"; public static final String DISABLE_CHECKSUMS_SETTING = "disableChecksums"; public static final String SYSTEM_PROPERTIES_SETTING = "systemProperties"; + private static final String GRAPE_IMPL_SYSTEM_PROPERTY = "groovy.grape.impl"; + private static final String DEFAULT_GRAPE_ENGINE = "groovy.grape.ivy.GrapeIvy"; private static final URI[] EMPTY_URI_ARRAY = new URI[0]; private static final Map[] EMPTY_MAP_ARRAY = new Map[0]; @@ -118,16 +122,89 @@ public static void setDisableChecksums(boolean disableChecksums) { public static synchronized GrapeEngine getInstance() { if (instance == null) { - try { - // by default use GrapeIvy - // TODO: META-INF/services resolver? - instance = (GrapeEngine) Class.forName("groovy.grape.GrapeIvy").getDeclaredConstructor().newInstance(); - } catch (ReflectiveOperationException ignore) { + String configuredImpl = System.getProperty(GRAPE_IMPL_SYSTEM_PROPERTY); + ServiceLoader.Provider provider = findProvider(configuredImpl); + if (provider != null) { + instance = createEngineFromProvider(provider); + } + if (instance == null) { + System.err.println("Grape: Grapes disabled."); } } return instance; } + private static ServiceLoader.Provider findProvider(final String configuredImpl) { + List> providers; + try { + ClassLoader grapeClassLoader = Grape.class.getClassLoader(); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + + // Keep deterministic order while avoiding duplicate provider types when both + // class loaders expose the same service entry. + Map> discovered = new LinkedHashMap<>(); + ServiceLoader.load(GrapeEngine.class, grapeClassLoader).stream() + .forEach(p -> discovered.putIfAbsent(p.type().getName(), p)); + if (contextClassLoader != null && contextClassLoader != grapeClassLoader) { + ServiceLoader.load(GrapeEngine.class, contextClassLoader).stream() + .forEach(p -> discovered.putIfAbsent(p.type().getName(), p)); + } + providers = discovered.values().stream().toList(); + } catch (ServiceConfigurationError sce) { + System.err.println("Grape: failed to discover service providers for " + GrapeEngine.class.getName() + ": " + sce.getMessage()); + return null; + } + + if (configuredImpl != null) { + for (ServiceLoader.Provider provider : providers) { + if (provider.type().getName().equals(configuredImpl)) { + providers.stream() + .filter(p -> !p.type().getName().equals(configuredImpl)) + .forEach(p -> System.err.println("Grape: ignoring provider '" + p.type().getName() + + "' ('" + configuredImpl + "' configured via -D" + GRAPE_IMPL_SYSTEM_PROPERTY + ").")); + return provider; + } + } + System.err.println("Grape: configured implementation '" + configuredImpl + + "' not found via service loader."); + return null; + } + + if (providers.size() == 1) { + return providers.get(0); + } + + if (providers.size() > 1) { + for (ServiceLoader.Provider provider : providers) { + if (provider.type().getName().equals(DEFAULT_GRAPE_ENGINE)) { + providers.stream() + .filter(p -> !p.type().getName().equals(DEFAULT_GRAPE_ENGINE)) + .forEach(p -> System.err.println("Grape: ignoring provider '" + p.type().getName() + + "' in favour of default '" + DEFAULT_GRAPE_ENGINE + + "' (set -D" + GRAPE_IMPL_SYSTEM_PROPERTY + " to override).")); + return provider; + } + } + // Multiple providers discovered but the default is not among them. + List names = providers.stream().map(p -> p.type().getName()).toList(); + System.err.println("Grape: " + providers.size() + " providers discovered " + names + + " but default '" + DEFAULT_GRAPE_ENGINE + "' is not among them;" + + " set -D" + GRAPE_IMPL_SYSTEM_PROPERTY + " to select one."); + } + + // No system property set: empty list means security lockdown — return null silently. + return null; + } + + private static GrapeEngine createEngineFromProvider(final ServiceLoader.Provider provider) { + try { + return provider.get(); + } catch (ServiceConfigurationError sce) { + System.err.println("Grape: failed to instantiate service provider '" + provider.type().getName() + "': " + sce.getMessage()); + return null; + } + } + public static void grab(String endorsed) { if (enableGrapes) { GrapeEngine instance = getInstance(); @@ -152,28 +229,21 @@ public static void grab(Map dependency) { } } - @SuppressWarnings("removal") // TODO a future Groovy version should perform the operation not as a privileged action public static void grab(final Map args, final Map... dependencies) { if (enableGrapes) { - java.security.AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Void run() { - GrapeEngine instance1 = getInstance(); - if (instance1 != null) { - if (!args.containsKey(AUTO_DOWNLOAD_SETTING)) { - args.put(AUTO_DOWNLOAD_SETTING, enableAutoDownload); - } - if (!args.containsKey(DISABLE_CHECKSUMS_SETTING)) { - args.put(DISABLE_CHECKSUMS_SETTING, disableChecksums); - } - if (!args.containsKey(GrapeEngine.CALLEE_DEPTH)) { - args.put(GrapeEngine.CALLEE_DEPTH, GrapeEngine.DEFAULT_CALLEE_DEPTH + 2); - } - instance1.grab(args, dependencies); - } - return null; + GrapeEngine instance1 = getInstance(); + if (instance1 != null) { + if (!args.containsKey(AUTO_DOWNLOAD_SETTING)) { + args.put(AUTO_DOWNLOAD_SETTING, enableAutoDownload); + } + if (!args.containsKey(DISABLE_CHECKSUMS_SETTING)) { + args.put(DISABLE_CHECKSUMS_SETTING, disableChecksums); } - }); + if (!args.containsKey(GrapeEngine.CALLEE_DEPTH)) { + args.put(GrapeEngine.CALLEE_DEPTH, GrapeEngine.DEFAULT_CALLEE_DEPTH + 2); + } + instance1.grab(args, dependencies); + } } } @@ -237,4 +307,5 @@ public static void addResolver(Map args) { } } } + } diff --git a/src/main/java/groovy/grape/GrapeEngine.java b/src/main/java/groovy/grape/GrapeEngine.java index aa11e23b7da..4255fbf2cdc 100644 --- a/src/main/java/groovy/grape/GrapeEngine.java +++ b/src/main/java/groovy/grape/GrapeEngine.java @@ -43,5 +43,11 @@ public interface GrapeEngine { Map[] listDependencies(ClassLoader classLoader); void addResolver(Map args); + + /** + * Sets the logging level for the grape engine. + * @param level the logging level (0=quiet/errors only, 1=warn, 2=info, 3=verbose, 4=debug) + */ + default void setLoggingLevel(int level) { } } diff --git a/src/spec/test/DesignPatternsTest.groovy b/src/spec/test/DesignPatternsTest.groovy index 79f453c047c..9bd3b918fa3 100644 --- a/src/spec/test/DesignPatternsTest.groovy +++ b/src/spec/test/DesignPatternsTest.groovy @@ -918,7 +918,8 @@ final class DesignPatternsTest extends CompilableTestSupport { @Test void testDecoratorSql() { shouldCompile ''' - @Grab('org.apache.groovy:groovy-sql:4.0.0-beta-2') + @GrabExclude('org.codehaus.groovy:groovy-all') + @Grab('org.apache.groovy:groovy-sql:5.0.4') import groovy.sql.Sql import java.lang.reflect.InvocationHandler import java.sql.Connection @@ -971,8 +972,9 @@ final class DesignPatternsTest extends CompilableTestSupport { @Test void testDecoratorGpars() { shouldCompile ''' + @GrabExclude('org.codehaus.groovy:groovy-all') // tag::decorator_gpars[] - @Grab('org.codehaus.gpars:gpars:0.10') + @Grab('org.codehaus.gpars:gpars:1.2.1') import static groovyx.gpars.GParsPool.withPool interface Document { diff --git a/src/test/groovy/bugs/Groovy10461.groovy b/src/test/groovy/bugs/Groovy10461.groovy index cb510dde61a..b0ee19f7729 100644 --- a/src/test/groovy/bugs/Groovy10461.groovy +++ b/src/test/groovy/bugs/Groovy10461.groovy @@ -28,6 +28,7 @@ final class Groovy10461 { void testParseClassWithOptionalDependencies() { assertScript ''' @GrabExclude('org.codehaus.groovy:groovy') + @GrabExclude('org.codehaus.groovy:groovy-all') @GrabExclude('org.codehaus.groovy:groovy-xml') @GrabExclude('org.codehaus.groovy:groovy-json') diff --git a/src/test/groovy/groovy/grape/GrabErrorIsolationTest.groovy b/src/test/groovy/groovy/grape/GrabErrorIsolationTest.groovy index abe7c07d3c6..3b61770a694 100644 --- a/src/test/groovy/groovy/grape/GrabErrorIsolationTest.groovy +++ b/src/test/groovy/groovy/grape/GrabErrorIsolationTest.groovy @@ -47,7 +47,8 @@ class GrabErrorIsolationTest { fail('This @Grab usage should have failed') } catch (ex) { // fine if it failed - assert ex.message.contains('unresolved dependency') + assert ex.message.contains('unresolved dependency') || + ex.message.contains('could not be resolved') } } diff --git a/src/test/groovy/groovy/grape/GrabResolverTest.groovy b/src/test/groovy/groovy/grape/GrabResolverTest.groovy index 7ad931c5b23..b36e39e7797 100644 --- a/src/test/groovy/groovy/grape/GrabResolverTest.groovy +++ b/src/test/groovy/groovy/grape/GrabResolverTest.groovy @@ -49,10 +49,16 @@ final class GrabResolverTest { assert grapeRoot.mkdir() grapeRoot.deleteOnExit() - Grape.instance.settings.getResolver('downloadGrapes').resolvers.removeAll { - // jcenter is no longer used but it is left in this test just in case - // someone running this test has an old ~/.groovy/grapeConfig.xml - it.name == 'localm2' || it.name == 'cachedGrapes' || it.name == 'jcenter' + def engine = Grape.instance + if (engine.getClass().name == 'groovy.grape.ivy.GrapeIvy') { + engine.settings.getResolver('downloadGrapes').resolvers.removeAll { + // jcenter is no longer used but it is left in this test just in case + // someone running this test has an old ~/.groovy/grapeConfig.xml + it.name == 'localm2' || it.name == 'cachedGrapes' || it.name == 'jcenter' + } + } else if (engine.metaClass.hasProperty(engine, 'repos')) { + // Maven engine: start from no repositories so @GrabResolver is required by the test. + engine.repos.clear() } } diff --git a/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy b/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy index f9936137d61..62384e52233 100644 --- a/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy +++ b/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy @@ -53,7 +53,8 @@ final class ExtensionModuleHelperForTests { def ant = new AntBuilder() def allowed = [ 'Picked up JAVA_TOOL_OPTIONS: .*', - 'Picked up _JAVA_OPTIONS: .*' + 'Picked up _JAVA_OPTIONS: .*', + '(?s)SLF4J\\(W\\): Class path contains multiple SLF4J providers\\..*SLF4J\\(I\\): Actual provider is of type \\[[^\\n]+\\]' ] try { ant.with { diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy index ab45703cf3d..bcb96bb8580 100644 --- a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy @@ -3393,6 +3393,7 @@ final class TraitASTTransformationTest { System.setProperty('spock.iKnowWhatImDoing.disableGroovyVersionCheck','true') assertScript shell, """ @Grab('org.spockframework:spock-core:2.4-groovy-5.0') + @GrabExclude('org.codehaus.groovy:groovy-all') @GrabExclude('org.apache.groovy:*') import spock.lang.Specification diff --git a/subprojects/groovy-console/build.gradle b/subprojects/groovy-console/build.gradle index 93522971065..de8128e6edb 100644 --- a/subprojects/groovy-console/build.gradle +++ b/subprojects/groovy-console/build.gradle @@ -25,6 +25,7 @@ dependencies { api projects.groovySwing // SwingBuilder is used in public API implementation "com.github.javaparser:javaparser-core:${versions.javaParser}" implementation "org.ow2.asm:asm-util:${versions.asm}" + implementation projects.groovyGrapeIvy implementation projects.groovyTemplates implementation ("org.apache.ivy:ivy:${versions.ivy}") { transitive = false diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy index 2139b85fee3..a70d2166aaa 100644 --- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy +++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy @@ -107,6 +107,7 @@ import java.util.prefs.Preferences class Console implements CaretListener, HyperlinkListener, ComponentListener, FocusListener { static final String DEFAULT_SCRIPT_NAME_START = 'ConsoleScript' + private static final boolean DEBUG_GRAPE = Boolean.getBoolean('groovy.grape.debug') static prefs = Preferences.userNodeForPackage(Console) @@ -375,7 +376,20 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo def ivyPluginClass = Class.forName('groovy.console.ui.ConsoleIvyPlugin') ivyPluginClass.getConstructor().newInstance().addListener(this) } - } catch (ClassNotFoundException ignore) { + } catch (ReflectiveOperationException ignore) { + if (DEBUG_GRAPE) { + System.err.println "Ignoring attempt to load ConsoleIvyPlugin: ${ignore.message}" + } + } + + // listen for Maven resolver events if the Maven console plugin is on the classpath + try { + def mavenPluginClass = Class.forName('groovy.console.ui.ConsoleMavenPlugin') + mavenPluginClass.getConstructor().newInstance().addListener(this) + } catch (ReflectiveOperationException ignore) { + if (DEBUG_GRAPE) { + System.err.println "Ignoring attempt to load ConsoleMavenPlugin: ${ignore.message}" + } } binding.variables._outputTransforms = OutputTransforms.loadOutputTransforms() diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleIvyPlugin.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleIvyPlugin.groovy index 9af8fce7598..8d16c2219c1 100644 --- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleIvyPlugin.groovy +++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleIvyPlugin.groovy @@ -19,7 +19,8 @@ package groovy.console.ui import groovy.grape.Grape -import groovy.grape.GrapeIvy +import groovy.grape.GrapeEngine +import groovy.grape.ivy.GrapeIvy import org.apache.ivy.core.event.IvyListener import org.apache.ivy.core.event.download.PrepareDownloadEvent import org.apache.ivy.core.event.resolve.StartResolveEvent @@ -36,29 +37,30 @@ class ConsoleIvyPlugin { def addListener(Console console) { savedConsole = console - - ((GrapeIvy) Grape.instance).ivyInstance.eventManager.addIvyListener([progress: { ivyEvent -> - switch (ivyEvent) { - case StartResolveEvent: - ivyEvent.moduleDescriptor.dependencies.each { it -> - def name = it.toString() - if (!resolvedDependencies.contains(name)) { - resolvedDependencies << name - savedConsole.showMessage "Resolving ${name} ..." + GrapeEngine engine = Grape.instance + if (engine instanceof GrapeIvy) { + engine.ivyInstance.eventManager.addIvyListener([progress: { ivyEvent -> + switch (ivyEvent) { + case StartResolveEvent: + ivyEvent.moduleDescriptor.dependencies.each { it -> + def name = it.toString() + if (!resolvedDependencies.contains(name)) { + resolvedDependencies << name + savedConsole.showMessage "Resolving ${name} ..." + } } - } - break - case PrepareDownloadEvent: - ivyEvent.artifacts.each { it -> - def name = it.toString() - if (!downloadedArtifacts.contains(name)) { - downloadedArtifacts << name - savedConsole.showMessage "Downloading artifact ${name} ..." + break + case PrepareDownloadEvent: + ivyEvent.artifacts.each { it -> + def name = it.toString() + if (!downloadedArtifacts.contains(name)) { + downloadedArtifacts << name + savedConsole.showMessage "Downloading artifact ${name} ..." + } } - } - break - } - }] as IvyListener) - + break + } + }] as IvyListener) + } } } diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleMavenPlugin.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleMavenPlugin.groovy new file mode 100644 index 00000000000..e99ff2d2d5f --- /dev/null +++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ConsoleMavenPlugin.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.console.ui + +import groovy.grape.Grape +import groovy.grape.GrapeEngine + +/** + * Adds Groovy Grape feedback for the Maven Resolver based engine. + */ +class ConsoleMavenPlugin { + Console savedConsole + Set resolvedDependencies = [] + Set downloadedArtifacts = [] + + def addListener(Console console) { + savedConsole = console + GrapeEngine engine = Grape.instance + if (engine?.class?.name == 'groovy.grape.maven.GrapeMaven' && engine.metaClass.respondsTo(engine, 'addProgressListener', Closure)) { + engine.addProgressListener { Map event -> + switch (event.type) { + case 'resolving': + if (resolvedDependencies.add(event.name)) { + savedConsole.showMessage "Resolving ${event.name} ..." + } + break + case 'downloading': + if (downloadedArtifacts.add(event.name)) { + savedConsole.showMessage "Downloading artifact ${event.name} ..." + } + break + } + } + } + } +} + diff --git a/subprojects/groovy-grape-ivy/build.gradle b/subprojects/groovy-grape-ivy/build.gradle new file mode 100644 index 00000000000..7283b13bc04 --- /dev/null +++ b/subprojects/groovy-grape-ivy/build.gradle @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +plugins { + id 'org.apache.groovy-library' +} + +dependencies { + api rootProject + implementation("org.apache.ivy:ivy:${versions.ivy}") { + transitive = false + } + testImplementation projects.groovyTest +} diff --git a/src/main/groovy/groovy/grape/GrapeIvy.groovy b/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy similarity index 80% rename from src/main/groovy/groovy/grape/GrapeIvy.groovy rename to subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy index b2d6a61217f..d79e0f8a82f 100644 --- a/src/main/groovy/groovy/grape/GrapeIvy.groovy +++ b/subprojects/groovy-grape-ivy/src/main/groovy/groovy/grape/ivy/GrapeIvy.groovy @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package groovy.grape +package groovy.grape.ivy + +import groovy.grape.GrapeEngine +import groovy.grape.GrapeUtil import groovy.transform.AutoFinal import groovy.transform.CompileDynamic @@ -24,8 +27,6 @@ import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.NamedParam import groovy.transform.NamedParams -import org.apache.groovy.plugin.GroovyRunner -import org.apache.groovy.plugin.GroovyRunnerRegistry import org.apache.ivy.Ivy import org.apache.ivy.core.IvyContext import org.apache.ivy.core.event.download.PrepareDownloadEvent @@ -49,20 +50,12 @@ import org.apache.ivy.plugins.resolver.IBiblioResolver import org.apache.ivy.plugins.resolver.ResolverSettings import org.apache.ivy.util.DefaultMessageLogger import org.apache.ivy.util.Message -import org.codehaus.groovy.reflection.CachedClass -import org.codehaus.groovy.reflection.ClassInfo import org.codehaus.groovy.reflection.ReflectionUtils -import org.codehaus.groovy.runtime.m12n.ExtensionModuleScanner -import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl import org.w3c.dom.Element import javax.xml.parsers.DocumentBuilderFactory import java.text.ParseException -import java.util.jar.JarFile import java.util.regex.Pattern -import java.util.zip.ZipEntry -import java.util.zip.ZipException -import java.util.zip.ZipFile /** * Implementation supporting {@code @Grape} and {@code @Grab} annotations based on Ivy. @@ -70,8 +63,6 @@ import java.util.zip.ZipFile @AutoFinal @CompileStatic class GrapeIvy implements GrapeEngine { - private static final String METAINF_PREFIX = 'META-INF/services/' - private static final String RUNNER_PROVIDER_CONFIG = GroovyRunner.getName() private static final List DEFAULT_CONF = Collections.singletonList('default') private static final Map> MUTUALLY_EXCLUSIVE_KEYS = processGrabArgs([ ['group', 'groupId', 'organisation', 'organization', 'org'], @@ -171,18 +162,34 @@ class GrapeIvy implements GrapeEngine { ClassLoader chooseClassLoader(Map args) { ClassLoader loader = (ClassLoader) args.classLoader if (!isValidTargetClassLoader(loader)) { - Class caller = args.refObject?.getClass() ?: - ReflectionUtils.getCallingClass((int) args.calleeDepth ?: 1) + int calleeDepth = (int) (args.calleeDepth ?: 1) + Class caller = args.refObject?.getClass() ?: ReflectionUtils.getCallingClass(calleeDepth) loader = caller?.getClassLoader() while (loader && !isValidTargetClassLoader(loader)) { loader = loader.getParent() } - /*if (!isValidTargetClassLoader(loader)) { + // Some call paths (e.g. instance-style static dispatch) can shift frames. + if (!isValidTargetClassLoader(loader)) { + int from = Math.max(1, calleeDepth - 2) + int to = calleeDepth + 2 + for (int i = from; i <= to && !isValidTargetClassLoader(loader); i += 1) { + Class alt = ReflectionUtils.getCallingClass(i) + ClassLoader altLoader = alt?.getClassLoader() + while (altLoader && !isValidTargetClassLoader(altLoader)) { + altLoader = altLoader.getParent() + } + if (isValidTargetClassLoader(altLoader)) { + loader = altLoader + } + } + } + // Refless/static contexts can hide the script loader from stack walking. + if (!isValidTargetClassLoader(loader)) { loader = Thread.currentThread().getContextClassLoader() } if (!isValidTargetClassLoader(loader)) { - loader = GrapeIvy.getClass().getClassLoader() - }*/ + loader = GrapeIvy.class.getClassLoader() + } if (!isValidTargetClassLoader(loader)) { throw new RuntimeException('No suitable ClassLoader found for grab') } @@ -287,20 +294,20 @@ class GrapeIvy implements GrapeEngine { URI[] uris = resolve(loader, args, dependencies) for (URI uri : uris) { - addURL(loader, uri) + GrapeUtil.addURL(loader, uri) } boolean runnerServicesFound = false for (URI uri : uris) { // TODO: check artifact type, jar vs library, etc. File file = new File(uri) - processCategoryMethods(loader, file) - Collection services = processMetaInfServices(loader, file) + GrapeUtil.processExtensionMethods(loader, file) + Collection services = GrapeUtil.processMetaInfServices(loader, file) if (!runnerServicesFound) { - runnerServicesFound = services.contains(RUNNER_PROVIDER_CONFIG) + runnerServicesFound = GrapeUtil.checkForRunner(services) } } if (runnerServicesFound) { - GroovyRunnerRegistry.getInstance().load(loader) + GrapeUtil.registryLoad(loader) } } catch (Exception e) { // clean-up the state first @@ -316,114 +323,6 @@ class GrapeIvy implements GrapeEngine { null } - @CompileDynamic - private void addURL(ClassLoader loader, URI uri) { - loader.addURL(uri.toURL()) - } - - private processCategoryMethods(ClassLoader loader, File file) { - // register extension methods if jar - if (file.getName().toLowerCase().endsWith('.jar')) { - def mcRegistry = GroovySystem.getMetaClassRegistry() - if (mcRegistry instanceof MetaClassRegistryImpl) { - try (JarFile jar = new JarFile(file)) { - ZipEntry entry = jar.getEntry(ExtensionModuleScanner.MODULE_META_INF_FILE) - if (!entry) { - entry = jar.getEntry(ExtensionModuleScanner.LEGACY_MODULE_META_INF_FILE) - } - if (entry) { - Properties props = new Properties() - - try (InputStream is = jar.getInputStream(entry)) { - props.load(is) - } - - Map> metaMethods = [:] - mcRegistry.registerExtensionModuleFromProperties(props, loader, metaMethods) - // add old methods to the map - metaMethods.each { CachedClass c, List methods -> - // GROOVY-5543: if a module was loaded using grab, there are chances that subclasses - // have their own ClassInfo, and we must change them as well! - Set classesToBeUpdated = [c].toSet() - ClassInfo.onAllClassInfo { ClassInfo info -> - if (c.getTheClass().isAssignableFrom(info.getCachedClass().getTheClass())) { - classesToBeUpdated << info.getCachedClass() - } - } - classesToBeUpdated*.addNewMopMethods(methods) - } - } - } catch (ZipException e) { - throw new RuntimeException("Grape could not load jar '$file'", e) - } - } - } - } - - void processOtherServices(ClassLoader loader, File f) { - processMetaInfServices(loader, f) // ignore result - } - - /** - * Searches the given File for known service provider - * configuration files to process. - * - * @param loader used to locate service provider files - * @param f ZipFile in which to search for services - * @return a collection of service provider files that were found - */ - private Collection processMetaInfServices(ClassLoader loader, File f) { - List services = [] - try (ZipFile zf = new ZipFile(f)) { - String providerConfig = 'org.codehaus.groovy.runtime.SerializedCategoryMethods' - ZipEntry serializedCategoryMethods = zf.getEntry(METAINF_PREFIX + providerConfig) - if (serializedCategoryMethods != null) { - services.add(providerConfig) - - try (InputStream is = zf.getInputStream(serializedCategoryMethods)) { - processSerializedCategoryMethods(is) - } - } - // TODO: remove in a future release (replaced by GroovyRunnerRegistry) - providerConfig = 'org.codehaus.groovy.plugins.Runners' - ZipEntry pluginRunners = zf.getEntry(METAINF_PREFIX + providerConfig) - if (pluginRunners != null) { - services.add(providerConfig) - - try (InputStream is = zf.getInputStream(pluginRunners)) { - processRunners(is, f.getName(), loader) - } - } - // GroovyRunners are loaded per ClassLoader using a ServiceLoader so here - // it only needs to be indicated that the service provider file was found - if (zf.getEntry(METAINF_PREFIX + RUNNER_PROVIDER_CONFIG) != null) { - services.add(RUNNER_PROVIDER_CONFIG) - } - } catch (ZipException ignore) { - // ignore files we can't process, e.g. non-jar/zip artifacts - // TODO: log a warning - } - services - } - - void processSerializedCategoryMethods(InputStream is) { - is.getText().readLines().each { - System.err.println(it.trim()) // TODO: implement this or delete it - } - } - - void processRunners(InputStream is, String name, ClassLoader loader) { - GroovyRunnerRegistry registry = GroovyRunnerRegistry.getInstance() - is.getText().readLines()*.trim().each { String line -> - if (!line.isEmpty() && line[0] != '#') - try { - registry[name] = (GroovyRunner) loader.loadClass(line).getDeclaredConstructor().newInstance() - } catch (Exception e) { - throw new IllegalStateException("Error registering runner class '$line'", e) - } - } - } - ResolveReport getDependencies(Map args, IvyGrabRecord... grabRecords) { def cacheManager = ivyInstance.getResolutionCacheManager() def millis = System.currentTimeMillis() @@ -730,6 +629,33 @@ class GrapeIvy implements GrapeEngine { resolvedDependencies = [] downloadedArtifacts = [] } + + @Override + void setLoggingLevel(int level) { + // Map numeric level to Ivy logging level + // 0=quiet/errors only, 1=warn, 2=info, 3=verbose, 4=debug + int ivyLevel + switch (level) { + case 0: + ivyLevel = Message.MSG_ERR + break + case 1: + ivyLevel = Message.MSG_WARN + break + case 2: + ivyLevel = Message.MSG_INFO + break + case 3: + ivyLevel = Message.MSG_VERBOSE + break + case 4: + ivyLevel = Message.MSG_DEBUG + break + default: + ivyLevel = Message.MSG_INFO + } + Message.setDefaultLogger(new DefaultMessageLogger(ivyLevel)) + } } @CompileStatic diff --git a/subprojects/groovy-grape-ivy/src/main/resources/META-INF/services/groovy.grape.GrapeEngine b/subprojects/groovy-grape-ivy/src/main/resources/META-INF/services/groovy.grape.GrapeEngine new file mode 100644 index 00000000000..9b39aba9a51 --- /dev/null +++ b/subprojects/groovy-grape-ivy/src/main/resources/META-INF/services/groovy.grape.GrapeEngine @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +groovy.grape.ivy.GrapeIvy diff --git a/src/resources/groovy/grape/defaultGrapeConfig.xml b/subprojects/groovy-grape-ivy/src/main/resources/groovy/grape/ivy/defaultGrapeConfig.xml similarity index 100% rename from src/resources/groovy/grape/defaultGrapeConfig.xml rename to subprojects/groovy-grape-ivy/src/main/resources/groovy/grape/ivy/defaultGrapeConfig.xml diff --git a/src/test/groovy/groovy/grape/GrapeIvyTest.groovy b/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeIvyTest.groovy similarity index 89% rename from src/test/groovy/groovy/grape/GrapeIvyTest.groovy rename to subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeIvyTest.groovy index d62fa43d667..40def144bf5 100644 --- a/src/test/groovy/groovy/grape/GrapeIvyTest.groovy +++ b/subprojects/groovy-grape-ivy/src/test/groovy/groovy/grape/ivy/GrapeIvyTest.groovy @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package groovy.grape +package groovy.grape.ivy + +import groovy.grape.Grape import org.codehaus.groovy.control.CompilationFailedException import org.junit.jupiter.api.BeforeAll @@ -89,6 +91,7 @@ final class GrapeIvyTest { @Test void testListDependencies() { + assumeTrue(Grape.instance instanceof GrapeIvy) // GrapeMaven has different conf rules def shell = new GroovyShell(new GroovyClassLoader()) shouldFail(CompilationFailedException) { shell.evaluate('import com.jidesoft.swing.JideSplitButton; JideSplitButton.class') @@ -193,6 +196,7 @@ final class GrapeIvyTest { @Test void testSerialGrabs() { + assumeTrue(Grape.instance instanceof GrapeIvy) // GrapeMaven has different rules dups GroovyClassLoader loader = new GroovyClassLoader() Grape.grab(groupId:'log4j', artifactId:'log4j', version:'1.1.3', classLoader:loader) Grape.grab(groupId:'org.apache.poi', artifactId:'poi', version:'3.7', classLoader:loader) @@ -211,6 +215,7 @@ final class GrapeIvyTest { @Test void testConf1() { + assumeTrue(Grape.instance instanceof GrapeIvy) // GrapeMaven has different conf rules Set noJars = [ ] Set coreJars = [ @@ -260,8 +265,10 @@ final class GrapeIvyTest { @Test // GROOVY-8372 void testConf2() { + assumeTrue(Grape.instance instanceof GrapeIvy) // only GrapeIvy uses def tempDir = File.createTempDir() - def jarsDir = new File(tempDir, 'foo/bar/jars'); jarsDir.mkdirs() + def jarsDir = new File(tempDir, 'foo/bar/jars') + jarsDir.mkdirs() new File(jarsDir, 'bar-1.2.3.jar').createNewFile() new File(jarsDir, 'baz-1.2.3.jar').createNewFile() @@ -324,6 +331,7 @@ final class GrapeIvyTest { @Test void testClassifierWithConf() { + assumeTrue(Grape.instance instanceof GrapeIvy) // GrapeMaven has different conf rules Set coreJars = [ 'json-lib-2.2.3-jdk15.jar', 'commons-beanutils-1.7.0.jar', @@ -354,13 +362,28 @@ final class GrapeIvyTest { Grape.grab(groupId:'net.sf.json-lib', artifactId:'json-lib', version:'2.2.3', classifier:'jdk15', classLoader:loader) assert jarNames(loader) == coreJars - loader = new GroovyClassLoader() - Grape.grab(groupId:'net.sf.json-lib', artifactId:'json-lib', version:'2.2.3', classifier:'jdk15', conf:'optional', classLoader:loader) - assert jarNames(loader) == optionalJars + try { + loader = new GroovyClassLoader() + Grape.grab(groupId:'net.sf.json-lib', artifactId:'json-lib', version:'2.2.3', classifier:'jdk15', conf:'optional', classLoader:loader) + assert jarNames(loader) == optionalJars + + loader = new GroovyClassLoader() + Grape.grab(groupId:'net.sf.json-lib', artifactId:'json-lib', version:'2.2.3', classifier:'jdk15', conf:['default', 'optional'], classLoader:loader) + assert jarNames(loader) == coreJars + optionalJars + } catch (RuntimeException e) { + // Some legacy optional transitive artifacts for this coordinate occasionally + // disappear from mirrors (e.g. ant-launcher:1.7.0, jdom:1.0). When that + // happens, the optional-conf assertion becomes an external-network issue. + assumeTrue(!isKnownLegacyOptionalDownloadFailure(e), + "Skipping due to unavailable legacy optional artifacts: ${e.message}") + throw e + } + } - loader = new GroovyClassLoader() - Grape.grab(groupId:'net.sf.json-lib', artifactId:'json-lib', version:'2.2.3', classifier:'jdk15', conf:['default', 'optional'], classLoader:loader) - assert jarNames(loader) == coreJars + optionalJars + private static boolean isKnownLegacyOptionalDownloadFailure(RuntimeException e) { + String msg = e?.message ?: '' + msg.contains('download failed: org.apache.ant#ant-launcher;1.7.0') || + msg.contains('download failed: org.jdom#jdom;1.0') } @Test // BeanUtils is a transitive dependency for Digester @@ -404,20 +427,21 @@ final class GrapeIvyTest { void testTransitiveShorthandExpectFailure() { shouldFail MissingPropertyException, ''' @Grab('org.apache.commons:commons-digester3:3.2;transitive=false') - import org.apache.commons.digester3.Digester + import org.apache.commons.digester3.annotations.utils.AnnotationUtils - assert Digester.name.size() == 37 + assert AnnotationUtils.name.size() == 62 assert org.apache.commons.beanutils.BeanUtils.name // cannot resolve ''' } @Test void testAutoDownloadGrapeConfigDefault() { + assumeTrue(Grape.instance instanceof GrapeIvy) // only GrapeIvy has ivyInstance property assertScript ''' @Grab('org.apache.commons:commons-digester3:3.2;transitive=false') - import org.apache.commons.digester3.Digester + import org.apache.commons.digester3.annotations.utils.AnnotationUtils - assert Digester.name.size() == 37 + assert AnnotationUtils.name.size() == 62 ''' assert Grape.instance.ivyInstance.settings.defaultResolver.name == 'downloadGrapes' } @@ -425,31 +449,34 @@ final class GrapeIvyTest { @Test void testAutoDownloadGrapeConfigFalse() { assumeFalse(System.getProperty('os.name').containsIgnoreCase('windows')) + assumeTrue(Grape.instance instanceof GrapeIvy) // only GrapeIvy has ivyInstance property assertScript ''' @Grab('org.apache.commons:commons-digester3:3.2;transitive=false') @GrabConfig(autoDownload=false) - import org.apache.commons.digester3.Digester + import org.apache.commons.digester3.annotations.utils.AnnotationUtils - assert Digester.name.size() == 37 + assert AnnotationUtils.name.size() == 62 ''' assert Grape.instance.ivyInstance.settings.defaultResolver.name == 'cachedGrapes' } @Test void testAutoDownloadGrapeConfigTrue() { + assumeTrue(Grape.instance instanceof GrapeIvy) // only GrapeIvy has ivyInstance property assertScript ''' @Grab('org.apache.commons:commons-digester3:3.2;transitive=false') @GrabConfig(autoDownload=true) - import org.apache.commons.digester3.Digester + import org.apache.commons.digester3.annotations.utils.AnnotationUtils - assert Digester.name.size() == 37 + assert AnnotationUtils.name.size() == 62 ''' assert Grape.instance.ivyInstance.settings.defaultResolver.name == 'downloadGrapes' } @Test // GROOVY-470: multiple jars should be loaded for an artifacts with and without a classifier void testClassifierAndNonClassifierOnSameArtifact() { + assumeTrue(Grape.instance instanceof GrapeIvy) // GrapeMaven is loading old version of Groovy GroovyClassLoader loader = new GroovyClassLoader() Grape.grab(groupId:'org.neo4j', artifactId:'neo4j-kernel', version:'2.0.0-RC1', classLoader:loader) Grape.grab(groupId:'org.neo4j', artifactId:'neo4j-kernel', version:'2.0.0-RC1', classifier:'tests', classLoader:loader) diff --git a/subprojects/groovy-grape-maven/build.gradle b/subprojects/groovy-grape-maven/build.gradle new file mode 100644 index 00000000000..3138fc8c903 --- /dev/null +++ b/subprojects/groovy-grape-maven/build.gradle @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +plugins { + id 'org.apache.groovy-library' +} + +dependencies { + api rootProject + implementation "org.apache.maven:maven-resolver-provider:${versions.mavenResolverProvider}" + implementation "org.apache.maven.resolver:maven-resolver-supplier-mvn4:${versions.mavenResolverSupplier}" + runtimeOnly "org.slf4j:slf4j-simple:${versions.slf4j}" +} diff --git a/subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy b/subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy new file mode 100644 index 00000000000..3fe9e5816e2 --- /dev/null +++ b/subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy @@ -0,0 +1,770 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape.maven + +import groovy.grape.GrapeEngine +import groovy.grape.GrapeUtil +import groovy.transform.AutoFinal +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic +import groovy.transform.NamedParam +import groovy.transform.NamedParams +import org.codehaus.groovy.reflection.ReflectionUtils +import org.eclipse.aether.AbstractRepositoryListener +import org.eclipse.aether.RepositoryEvent +import org.eclipse.aether.RepositorySystem +import org.eclipse.aether.RepositorySystemSession +import org.eclipse.aether.artifact.Artifact +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.collection.CollectRequest +import org.eclipse.aether.graph.Dependency +import org.eclipse.aether.graph.DependencyFilter +import org.eclipse.aether.graph.DependencyNode +import org.eclipse.aether.internal.impl.scope.OptionalDependencySelector +import org.eclipse.aether.internal.impl.scope.ScopeDependencySelector +import org.eclipse.aether.repository.LocalRepository +import org.eclipse.aether.repository.RemoteRepository +import org.eclipse.aether.repository.RepositoryPolicy +import org.eclipse.aether.resolution.ArtifactRequest +import org.eclipse.aether.resolution.ArtifactResult +import org.eclipse.aether.resolution.DependencyRequest +import org.eclipse.aether.resolution.DependencyResolutionException +import org.eclipse.aether.supplier.RepositorySystemSupplier +import org.eclipse.aether.transfer.AbstractTransferListener +import org.eclipse.aether.transfer.TransferEvent +import org.eclipse.aether.util.graph.selector.AndDependencySelector +import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy + +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Implementation supporting {@code @Grape} and {@code @Grab} annotations based on Maven. + */ +@AutoFinal +@CompileStatic +class GrapeMaven implements GrapeEngine { + private static final List DEFAULT_CONF = Collections.singletonList('default') + private static final Map> MUTUALLY_EXCLUSIVE_KEYS = processGrabArgs([ + ['group', 'groupId', 'organisation', 'organization', 'org'], + ['module', 'artifactId', 'artifact'], + ['version', 'revision', 'rev'], + ['conf', 'scope', 'configuration'], + ]) + private static final boolean DEBUG_GRAB = Boolean.getBoolean('groovy.grape.debug') + + static { + // Configure SLF4J Simple Logger early so Maven/Aether infrastructure logs at WARN + // by default, or DEBUG when groovy.grape.debug is set. + String level = DEBUG_GRAB ? 'debug' : 'warn' + System.setProperty('org.slf4j.simpleLogger.defaultLogLevel', level) + + // If SimpleLoggerFactory was already initialised (e.g. by the test framework), + // reset its internal logger cache so subsequent getLogger() calls pick up the + // new level. This is a best-effort reflection-only approach; if it fails the + // system-property set above will at least govern any not-yet-created loggers. + try { + Class lfc = Class.forName('org.slf4j.LoggerFactory') + Object ilf = lfc.getMethod('getILoggerFactory').invoke(null) + if (ilf != null && ilf.getClass().getName() == 'org.slf4j.simple.SimpleLoggerFactory') { + java.lang.reflect.Field f = ilf.getClass().getDeclaredField('loggerMap') + f.setAccessible(true) + ((java.util.Map) f.get(ilf)).clear() + } + } catch (Exception ignored) { + // Not using slf4j-simple, or reflection failed — best effort only + } + } + + @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c] + private static Map> processGrabArgs(List> grabArgs) { + grabArgs.inject([:]) { Map m, List g -> g.each { a -> m[a] = (g - a) as Set }; m } + } + + // weak hash map so we don't leak loaders directly + final Map> loadedDeps = [] as WeakHashMap + /** Stores the MavenGrabRecord(s) for all dependencies in each grab() call. */ + final Set grabRecordsForCurrDependencies = [] as LinkedHashSet + boolean enableGrapes = true + final List progressListeners = new CopyOnWriteArrayList<>() + final List repos = [ + new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build() + ] + + @CompileDynamic + void addProgressListener(Closure listener) { + if (listener != null) { + progressListeners.add(listener) + } + } + + void removeProgressListener(Closure listener) { + progressListeners.remove(listener) + } + + private boolean hasProgressListeners() { + !progressListeners.isEmpty() + } + + @CompileDynamic + private void fireProgressEvent(String type, String name) { + if (!name) return + Map event = [type: type, name: name] + progressListeners.each { Closure listener -> + listener.call(event) + } + } + + private AbstractRepositoryListener createRepositoryListener() { + new AbstractRepositoryListener() { + @Override + void artifactResolving(RepositoryEvent event) { + Artifact artifact = event?.artifact + if (artifact != null && artifact.extension != 'pom') { + fireProgressEvent('resolving', artifact.toString()) + } + } + } + } + + private AbstractTransferListener createTransferListener() { + new AbstractTransferListener() { + @Override + void transferInitiated(TransferEvent event) { + String resourceName = event?.resource?.resourceName + String displayName = displayDownloadName(resourceName) + if (displayName != null) { + fireProgressEvent('downloading', displayName) + } + } + } + } + + private static String displayDownloadName(String resourceName) { + if (!resourceName) return null + String fileName = resourceName.tokenize('/').last() + if (!fileName + || fileName.startsWith('maven-metadata') + || fileName.endsWith('.pom') + || fileName.endsWith('.sha1') + || fileName.endsWith('.md5') + || fileName.endsWith('.asc')) { + return null + } + fileName + } + + /** + * Grab the endorsed module for the current Groovy version. + */ + @Override + grab(String endorsedModule) { + grab(group: 'groovy.endorsed', module: endorsedModule, version: GroovySystem.getVersion()) + } + + @Override + grab(Map args) { + args.calleeDepth = args.calleeDepth ?: DEFAULT_CALLEE_DEPTH + 1 + grab(args, args) + } + + @Override + grab(Map args, Map... dependencies) { + ClassLoader loader = null + grabRecordsForCurrDependencies.clear() + + try { + // identify the target classloader early, so we fail before checking repositories + loader = chooseClassLoader( + refObject: args.remove('refObject'), + classLoader: args.remove('classLoader'), + calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH, + ) + + // check for non-fail null + // if we were in fail mode we would have already thrown an exception + if (!loader) return + + URI[] uris = resolve(loader, args, dependencies) + for (URI uri : uris) { + GrapeUtil.addURL(loader, uri) + } + boolean runnerServicesFound = false + for (URI uri : uris) { + // TODO: check artifact type, jar vs library, etc. + File file = new File(uri) + GrapeUtil.processExtensionMethods(loader, file) + Collection services = GrapeUtil.processMetaInfServices(loader, file) + if (!runnerServicesFound) { + runnerServicesFound = GrapeUtil.checkForRunner(services) + } + } + if (runnerServicesFound) { + GrapeUtil.registryLoad(loader) + } + } catch (Exception e) { + // clean-up the state first + Set grabRecordsForCurrLoader = getLoadedDepsForLoader(loader) + grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies) + grabRecordsForCurrDependencies.clear() + + if (args.noExceptions) { + return e + } + throw asRuntimeGrabError(e) + } + null + } + + + @Override + @CompileDynamic + Map>> enumerateGrapes() { + Map>> bunches = [:] + File cacheRoot = grapeCacheDir.canonicalFile + + cacheRoot.eachFileRecurse { File f -> + if (!f.isFile()) return + String name = f.name + if (name.endsWith('.sha1') || name.endsWith('.md5') || name.endsWith('.asc')) return + + File versionDir = f.parentFile + File moduleDir = versionDir?.parentFile + File groupDir = moduleDir?.parentFile + if (!versionDir || !moduleDir || !groupDir) return + + String version = versionDir.name + String module = moduleDir.name + String expectedPrefix = module + '-' + version + if (!name.startsWith(expectedPrefix)) return + + String groupPath = cacheRoot.toPath().relativize(groupDir.toPath()).toString() + if (!groupPath) return + String groupKey = groupPath.replace(File.separatorChar, '.' as char) + + Map> modules = bunches.computeIfAbsent(groupKey) { [:] } + List versions = modules.computeIfAbsent(module) { [] } + if (!versions.contains(version)) { + versions << version + } + } + + bunches.values()*.values()*.each { List versions -> versions.sort() } + bunches + } + + void uninstallArtifact(String group, String module, String rev) { + String groupPath = group.replace('.' as char, File.separatorChar) + def artifactDir = new File(grapeCacheDir, groupPath + File.separator + module + File.separator + rev) + if (artifactDir.exists()) { + artifactDir.deleteDir() + } + } + + @Override + URI[] resolve(Map args, Map... dependencies) { + resolve(args, null, dependencies) + } + + @Override + URI[] resolve(Map args, List depsInfo, Map... dependencies) { + // identify the target classloader early, so we fail before checking repositories + ClassLoader loader = chooseClassLoader( + refObject: args.remove('refObject'), + classLoader: args.remove('classLoader'), + calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH, + ) + + // check for non-fail null + // if we were in fail mode we would have already thrown an exception + if (!loader) { + return new URI[0] + } + + resolve(loader, args, depsInfo, dependencies) + } + + private Set getLoadedDepsForLoader(ClassLoader loader) { + // use a LinkedHashSet to preserve the initial insertion order + loadedDeps.computeIfAbsent(loader, k -> [] as LinkedHashSet) + } + + URI[] resolve(ClassLoader loader, Map args, Map... dependencies) { + resolve(loader, args, null, dependencies) + } + + URI[] resolve(ClassLoader loader, Map args, List depsInfo, Map... dependencies) { + if (!enableGrapes) { + return new URI[0] + } + + boolean populateDepsInfo = (depsInfo != null) + Set localDeps = getLoadedDepsForLoader(loader) + List grabRecords = [] + for (Map dep : dependencies) { + MavenGrabRecord mgr = createGrabRecord(dep) + grabRecordsForCurrDependencies.add(mgr) + localDeps.add(mgr) + grabRecords.add(mgr) + } + + try { + URI[] results = getDependencies(args, populateDepsInfo ? depsInfo : null, grabRecords as MavenGrabRecord[]) + return results + } catch (Exception e) { + localDeps.removeAll(grabRecordsForCurrDependencies) + grabRecordsForCurrDependencies.clear() + throw asRuntimeGrabError(e) + } + } + + URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord... grabRecords) { + try (RepositorySystem system = new RepositorySystemSupplier().get()) { + def localRepo = new LocalRepository(grapeCacheDir.toPath()) + String checksumPolicy = args.disableChecksums ? + RepositoryPolicy.CHECKSUM_POLICY_IGNORE : + RepositoryPolicy.CHECKSUM_POLICY_WARN + + // Extract exclusions from args if provided by @GrabExclude + List> exclusions = (List>) args.get('excludes') ?: [] + + try (RepositorySystemSession.CloseableSession session = system + .createSessionBuilder() + .withLocalRepositories(localRepo) + .setChecksumPolicy(checksumPolicy) + .setDependencySelector(new AndDependencySelector( + ScopeDependencySelector.legacy(['compile', 'runtime'], ['test']), + OptionalDependencySelector.fromRoot() + )) + .setRepositoryListener(hasProgressListeners() ? createRepositoryListener() : null) + .setTransferListener(hasProgressListeners() ? createTransferListener() : null) + .setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy(true, true)) + // Set Java version properties for profile evaluation + .setSystemProperty('java.version', System.getProperty('java.version')) + .setSystemProperty('java.home', System.getProperty('java.home')) + // Set OS detection properties (similar to os-maven-plugin) + .setSystemProperty('os.detected.name', detectOsName()) + .setSystemProperty('os.detected.arch', detectOsArch()) + .setSystemProperty('os.detected.classifier', "${detectOsName()}-${detectOsArch()}") + // Only the most essential configurations + .setSystemProperty('aether.artifactDescriptor.ignoreErrors', 'true') + .setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions', 'true') + .build()) { + + List results = [] + + for (MavenGrabRecord grabRecord : grabRecords) { + // Skip if this grab record is in the exclusion list + if (isExcluded(grabRecord, exclusions)) { + continue + } + + String coords = "${grabRecord.groupId()}:${grabRecord.module()}" + if (grabRecord.ext()) { + coords += ":${grabRecord.ext()}" + } + if (grabRecord.classifier()) { + coords += ":${grabRecord.classifier()}" + } + coords += ":${grabRecord.version()}" + + if (DEBUG_GRAB) { + System.err.println("[GrapeDebug] resolving root=${coords} transitive=${grabRecord.transitive()}") + } + + Artifact artifact = new DefaultArtifact(coords) + + String scope = grabRecord.conf()?.get(0) ?: 'compile' + if (scope == 'default') scope = 'compile' + + List artifactResults = [] + if (grabRecord.transitive()) { + // Create a filter that prevents traversal into excluded artifacts + DependencyFilter exclusionFilter = createExclusionFilter(exclusions) + + CollectRequest collectRequest = new CollectRequest( + root: new Dependency(artifact, scope), + repositories: repos + ) + DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, exclusionFilter) + try { + artifactResults = system.resolveDependencies(session, dependencyRequest).getArtifactResults() + } catch (DependencyResolutionException dre) { + // Keep resolved artifacts; tolerate unresolved non-root transitives. + artifactResults = [] + if (dre.result?.artifactResults) { + for (ArtifactResult ar : dre.result.artifactResults) { + if (ar.resolved) { + artifactResults << ar + continue + } + Artifact missing = ar?.request?.artifact + if (!isIgnorableUnresolvedArtifact(system, session, artifact, missing)) { + throw dre + } + } + } else { + throw dre + } + } + } else { + // Non-transitive resolution + ArtifactRequest artifactRequest = new ArtifactRequest(artifact, repos, null) + artifactResults = [system.resolveArtifact(session, artifactRequest)] + } + + for (ArtifactResult found : artifactResults) { + if (found.artifact.path) { + if (DEBUG_GRAB) { + System.err.println("[GrapeDebug] resolved ${found.artifact.groupId}:${found.artifact.artifactId}:${found.artifact.extension}:${found.artifact.version} -> ${found.artifact.path}") + } + results << found.artifact.path.toUri() + + if (depsInfo != null) { + depsInfo << [ + 'group': found.artifact.groupId, + 'module': found.artifact.artifactId, + 'revision': found.artifact.version + ] + } + } + } + } + + return results as URI[] + } + } + } + + private static DependencyFilter createExclusionFilter(List> exclusions) { + if (!exclusions) { + return null // No exclusions, no filtering needed + } + + return { DependencyNode node, Collection parents -> + // Return false to prevent traversal into excluded artifacts + // This stops their dependencies from being collected + return !isExcludedArtifact(node.artifact.groupId, node.artifact.artifactId, exclusions) + } as DependencyFilter + } + + private static boolean isExcluded(MavenGrabRecord grabRecord, List> exclusions) { + return isExcludedArtifact(grabRecord.groupId(), grabRecord.module(), exclusions) + } + + private static boolean isExcludedArtifact(String groupId, String artifactId, List> exclusions) { + for (Map exclusion : exclusions) { + String excludeGroup = exclusion.group ?: exclusion.groupId + String excludeModule = exclusion.module ?: exclusion.artifactId + + if (excludeGroup && excludeModule) { + if (groupId == excludeGroup && artifactId == excludeModule) { + return true // This artifact is excluded + } + } + } + return false + } + + static MavenGrabRecord createGrabRecord(Map dep) { + String module = dep.module ?: dep.artifactId ?: dep.artifact + if (!module) { + throw new RuntimeException('grab requires at least a module: or artifactId: or artifact: argument') + } + + // check for malformed components of the coordinates + dep.each { k, v -> + if (v instanceof CharSequence) { + if (k.toString().contains('v')) { // revision, version, rev + if (!(v ==~ '[^\\\\/:"<>|]*')) { + throw new RuntimeException("Grab: invalid value of '$v' for $k: should not contain any of / \\ : \" < > |") + } + } else { + if (!(v ==~ '[-._a-zA-Z0-9]*')) { + throw new RuntimeException("Grab: invalid value of '$v' for $k: should only contain - . _ a-z A-Z 0-9") + } + } + } + } + + // check for mutually exclusive arguments + Set keys = (Set) dep.keySet() + keys.each { key -> + Set badArgs = MUTUALLY_EXCLUSIVE_KEYS[key] + if (badArgs && !badArgs.disjoint(keys)) { + throw new RuntimeException("Grab: mutually exclusive arguments: ${keys.intersect(badArgs) + key}") + } + } + + String groupId = dep.group ?: dep.groupId ?: dep.organisation ?: dep.organization ?: dep.org ?: '' + String version = dep.version ?: dep.revision ?: dep.rev ?: '*' + if (version == '*') version = 'LATEST' + String classifier = dep.classifier ?: null + String ext = dep.ext ?: dep.type ?: 'jar' + String type = dep.type ?: '' + + boolean force = dep.containsKey('force') ? dep.force : true + boolean changing = dep.containsKey('changing') ? dep.changing : false + boolean transitive = dep.containsKey('transitive') ? dep.transitive : true + + new MavenGrabRecord( + groupId, + module, + version, + getConfList(dep), + ext, + type, + classifier, + force, + changing, + transitive + ) + } + + @CompileDynamic + private static List getConfList(Map dep) { + def conf = dep.conf ?: dep.scope ?: dep.configuration ?: DEFAULT_CONF + if (conf instanceof String) { + if (conf.startsWith('[') && conf.endsWith(']')) conf = conf[1..-2] + conf = conf.tokenize(',') + } + (conf as List) + } + + @Override + Map[] listDependencies(ClassLoader classLoader) { + List results = loadedDeps[classLoader]?.collect { MavenGrabRecord grabbed -> + def dep = [ + group : grabbed.groupId(), + module : grabbed.module(), + version: grabbed.version() + ] + if (grabbed.conf() != DEFAULT_CONF) { + dep.conf = grabbed.conf() + } + if (grabbed.changing()) { + dep.changing = grabbed.changing() + } + if (!grabbed.transitive()) { + dep.transitive = grabbed.transitive() + } + if (!grabbed.force()) { + dep.force = grabbed.force() + } + if (grabbed.classifier()) { + dep.classifier = grabbed.classifier() + } + if (grabbed.ext()) { + dep.ext = grabbed.ext() + } + if (grabbed.type()) { + dep.type = grabbed.type() + } + dep + } + results as Map[] + } + + @Override + void addResolver(@NamedParams([ + @NamedParam(value = 'name', type = String, required = true), + @NamedParam(value = 'root', type = String, required = true), + @NamedParam(value = 'm2Compatible', type = Boolean, required = false) + ]) Map args) { + String root = (String) (args.root ?: args.value) + if (!root) { + throw new RuntimeException("GrabResolver requires a root: or value: argument") + } + String name = (String) (args.name ?: "resolver-${repos.size()}") + RemoteRepository resolver = new RemoteRepository.Builder(name, "default", root).build() + // Keep parity with Ivy: most recently added resolver should be checked first. + repos.add(0, resolver) + } + + @Override + void setLoggingLevel(int level) { + // Maven Resolver uses SLF4J for logging, which is configured externally + // We could potentially adjust logging here if needed, but for now + // Maven Resolver logging is controlled via SLF4J configuration + // Level mapping: 0=quiet/errors, 1=warn, 2=info, 3=verbose, 4=debug + } + + static File getGroovyRoot() { + String root = System.getProperty('groovy.root') + def groovyRoot + if (root == null) { + groovyRoot = new File(System.getProperty('user.home'), '.groovy') + } else { + groovyRoot = new File(root) + } + try { + groovyRoot = groovyRoot.getCanonicalFile() + } catch (IOException ignore) { + // skip canonicalization then, it may not exist yet + } + groovyRoot + } + + static File getGrapeDir() { + String root = System.getProperty('grape.root') + if (root == null) { + return getGroovyRoot() + } + File grapeRoot = new File(root) + try { + grapeRoot = grapeRoot.getCanonicalFile() + } catch (IOException ignore) { + // skip canonicalization then, it may not exist yet + } + grapeRoot + } + + static File getGrapeCacheDir() { + File cache = new File(getGrapeDir(), 'grapesM2') + if (!cache.exists()) { + cache.mkdirs() + } else if (!cache.isDirectory()) { + throw new RuntimeException("The grape cache dir $cache is not a directory") + } + cache + } + + private ClassLoader chooseClassLoader(Map args) { + ClassLoader loader = (ClassLoader) args.classLoader + if (!isValidTargetClassLoader(loader)) { + Class caller = args.refObject?.getClass() ?: + ReflectionUtils.getCallingClass((int) args.calleeDepth ?: 1) + loader = caller?.getClassLoader() + while (loader && !isValidTargetClassLoader(loader)) { + loader = loader.getParent() + } + if (!isValidTargetClassLoader(loader)) { + throw new RuntimeException('No suitable ClassLoader found for grab') + } + } + loader + } + + private boolean isValidTargetClassLoader(ClassLoader loader) { + isValidTargetClassLoaderClass(loader?.getClass()) + } + + private boolean isValidTargetClassLoaderClass(Class loaderClass) { + loaderClass != null && (loaderClass.getName() == 'groovy.lang.GroovyClassLoader' + || loaderClass.getName() == 'org.codehaus.groovy.tools.RootLoader' + || isValidTargetClassLoaderClass(loaderClass.getSuperclass())) + } + + private static String detectOsName() { + String osName = System.getProperty('os.name').toLowerCase() + if (osName.contains('win')) { + return 'windows' + } else if (osName.contains('mac')) { + return 'osx' + } else if (osName.contains('linux')) { + return 'linux' + } else if (osName.contains('sunos')) { + return 'sunos' + } else if (osName.contains('freebsd')) { + return 'freebsd' + } + // Default fallback + return osName.replaceAll('[^a-zA-Z0-9]', '') + } + + private static String detectOsArch() { + String osArch = System.getProperty('os.arch').toLowerCase() + if (osArch.matches('.*64.*')) { + if (osArch.contains('aarch64') || osArch.contains('arm64')) { + return 'aarch_64' + } + return 'x86_64' + } + if (osArch.matches('.*86.*') || osArch == 'i386') { + return 'x86' + } + if (osArch.contains('ppc64')) { + return 'ppc64' + } + if (osArch.contains('ppc')) { + return 'ppc' + } + if (osArch.contains('s390')) { + return 's390' + } + if (osArch.contains('sparc')) { + return 'sparc' + } + // Default fallback + return osArch.replaceAll('[^a-zA-Z0-9]', '') + } + + private boolean isMissingJarButPomExists(RepositorySystem system, RepositorySystemSession session, Artifact artifact) { + if (artifact == null) return false + // We only treat missing jar-like artifacts as ignorable if corresponding POM exists. + if (artifact.extension != 'jar') return false + try { + Artifact pomArtifact = new DefaultArtifact(artifact.groupId, artifact.artifactId, 'pom', artifact.version) + ArtifactRequest pomReq = new ArtifactRequest(pomArtifact, repos, null) + ArtifactResult pomRes = system.resolveArtifact(session, pomReq) + return pomRes?.resolved + } catch (Exception ignore) { + return false + } + } + + private boolean isIgnorableUnresolvedArtifact(RepositorySystem system, + RepositorySystemSession session, + Artifact rootArtifact, + Artifact missingArtifact) { + if (missingArtifact == null) return false + // Never ignore unresolved root artifact. + if (rootArtifact != null + && missingArtifact.groupId == rootArtifact.groupId + && missingArtifact.artifactId == rootArtifact.artifactId + && missingArtifact.version == rootArtifact.version) { + return false + } + // Ignore classifier/native variants that are often platform-specific/optional. + if (missingArtifact.classifier) return true + // Ignore POM-only coordinates requested as jars. + if (isMissingJarButPomExists(system, session, missingArtifact)) return true + // For transitive-only misses, keep Ivy-like lenient behavior and continue. + true + } + + private static RuntimeException asRuntimeGrabError(Exception e) { + if (e instanceof RuntimeException) return (RuntimeException) e + String msg = e.message ?: e.class.name + return new RuntimeException("Error grabbing Grapes -- ${msg}", e) + } + +} + +record MavenGrabRecord( + String groupId, + String module, + String version, + List conf, + String ext, + String type, + String classifier, + boolean force, + boolean changing, + boolean transitive +) {} diff --git a/subprojects/groovy-grape-maven/src/main/resources/META-INF/services/groovy.grape.GrapeEngine b/subprojects/groovy-grape-maven/src/main/resources/META-INF/services/groovy.grape.GrapeEngine new file mode 100644 index 00000000000..121ac9d34da --- /dev/null +++ b/subprojects/groovy-grape-maven/src/main/resources/META-INF/services/groovy.grape.GrapeEngine @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +groovy.grape.maven.GrapeMaven diff --git a/subprojects/groovy-grape-maven/src/test/groovy/groovy/grape/maven/GrapeMavenTest.groovy b/subprojects/groovy-grape-maven/src/test/groovy/groovy/grape/maven/GrapeMavenTest.groovy new file mode 100644 index 00000000000..78c62643ac6 --- /dev/null +++ b/subprojects/groovy-grape-maven/src/test/groovy/groovy/grape/maven/GrapeMavenTest.groovy @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape.maven + +import groovy.grape.Grape +import org.junit.jupiter.api.Test + +import java.nio.file.Files +import java.util.jar.JarOutputStream + +final class GrapeMavenTest { + private static File writeEmptyJar(File jarFile) { + jarFile.parentFile.mkdirs() + jarFile.withOutputStream { os -> + new JarOutputStream(os).close() + } + jarFile + } + + private static void deleteCachedGroup(String groupId) { + File cachedGroupDir = new File(GrapeMaven.grapeCacheDir, groupId.replace('.' as char, File.separatorChar)) + if (cachedGroupDir.exists()) { + cachedGroupDir.deleteDir() + } + } + + private static File writePom(File pomFile, String groupId, String artifactId, String version, List> deps = []) { + pomFile.parentFile.mkdirs() + String depsXml = deps.collect { Map dep -> + String optionalXml = dep.optional ? '\n true' : '' + """\ + + ${dep.groupId} + ${dep.artifactId} + ${dep.version} + compile${optionalXml} + """.stripIndent() + }.join('\n') + + pomFile.text = ( +""" + + 4.0.0 + ${groupId} + ${artifactId} + ${version} + jar + +${depsXml} + + +""" + ) + pomFile + } + + private static void publishArtifact(File repoDir, String groupId, String artifactId, String version, List> deps = []) { + String relPath = groupId.replace('.', '/') + "/${artifactId}/${version}" + File artifactDir = new File(repoDir, relPath) + writePom(new File(artifactDir, "${artifactId}-${version}.pom"), groupId, artifactId, version, deps) + writeEmptyJar(new File(artifactDir, "${artifactId}-${version}.jar")) + } + + private static Set jarNames(GroovyClassLoader loader) { + loader.URLs.collect { url -> url.path.split('/')[-1] } as Set + } + + @Test + void testResolveReturnsUris() { + URI[] uris = Grape.resolve([autoDownload: true, classLoader: new GroovyClassLoader()], + [groupId: 'commons-logging', artifactId: 'commons-logging', version: '1.2']) + + assert uris.length > 0 + assert uris.any { it.toString().contains('commons-logging-1.2') } + } + + @Test + void testListDependenciesAfterGrab() { + def loader = new GroovyClassLoader() + + Grape.grab(classLoader: loader, + [groupId: 'commons-logging', artifactId: 'commons-logging', version: '1.2']) + + Map[] loadedDependencies = Grape.listDependencies(loader) + assert loadedDependencies.find { + it.group == 'commons-logging' && it.module == 'commons-logging' && it.version == '1.2' + } + } + + @Test + void testNonTransitiveGrabResolvesOnlyDirectArtifact() { + URI[] uris = Grape.resolve([autoDownload: true, classLoader: new GroovyClassLoader()], + [groupId: 'org.apache.httpcomponents', artifactId: 'httpclient', version: '4.5.14', transitive: false]) + + assert uris.any { it.toString().contains('httpclient-4.5.14') } + assert !uris.any { it.toString().contains('httpcore-') } + } + + @Test + void testClassifierResolvesJsonLibJdk15() { + def loader = new GroovyClassLoader() + + Grape.grab(classLoader: loader, + [groupId: 'net.sf.json-lib', artifactId: 'json-lib', version: '2.2.3', classifier: 'jdk15']) + + Set jars = jarNames(loader) + assert jars.any { it == 'json-lib-2.2.3-jdk15.jar' } + assert jars.any { it.startsWith('ezmorph-') } + } + + @Test + void testOptionalDependencyIsNotPulledTransitively() { + File tempRoot = Files.createTempDirectory('grape-maven-optional-test').toFile() + File repoDir = new File(tempRoot, 'repo') + + String g = 'dev.grape.optional' + deleteCachedGroup(g) + publishArtifact(repoDir, g, 'dep-required', '1.0.0') + publishArtifact(repoDir, g, 'dep-optional', '1.0.0') + publishArtifact(repoDir, g, 'root-artifact', '1.0.0', [ + [groupId: g, artifactId: 'dep-required', version: '1.0.0', optional: false], + [groupId: g, artifactId: 'dep-optional', version: '1.0.0', optional: true], + ]) + + Grape.addResolver(name: 'local-optional-test', root: repoDir.toURI().toString(), m2Compatible: true) + URI[] uris = Grape.resolve([autoDownload: true, classLoader: new GroovyClassLoader()], + [groupId: g, artifactId: 'root-artifact', version: '1.0.0']) + + assert uris.any { it.toString().contains('root-artifact-1.0.0.jar') } + assert uris.any { it.toString().contains('dep-required-1.0.0.jar') } + assert !uris.any { it.toString().contains('dep-optional-1.0.0.jar') } + } +} diff --git a/subprojects/groovy-grape-test/build.gradle b/subprojects/groovy-grape-test/build.gradle new file mode 100644 index 00000000000..05de679608b --- /dev/null +++ b/subprojects/groovy-grape-test/build.gradle @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +plugins { + id 'groovy' + id 'org.apache.groovy-common' + id 'org.apache.groovy-internal' + id 'org.apache.groovy-tested' +} + +dependencies { + testImplementation rootProject + testImplementation projects.groovyTest + testImplementation projects.groovyTestJunit5 + testRuntimeOnly projects.groovyGrapeIvy + testRuntimeOnly projects.groovyGrapeMaven +} + +tasks.named('test', Test) { + useJUnitPlatform() + forkEvery = 1 + maxParallelForks = 1 +} + diff --git a/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeConfiguredMavenSelectionTest.groovy b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeConfiguredMavenSelectionTest.groovy new file mode 100644 index 00000000000..1797444bcf2 --- /dev/null +++ b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeConfiguredMavenSelectionTest.groovy @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape + +import org.junit.jupiter.api.Test + +final class GrapeConfiguredMavenSelectionTest { + @Test + void testConfiguredMavenImplementationIgnoresIvyProvider() { + System.setProperty('groovy.grape.impl', 'groovy.grape.maven.GrapeMaven') + String output = GrapeSelectionTestSupport.captureStderr { + assert Grape.instance != null + } + + assert output.contains("ignoring provider 'groovy.grape.ivy.GrapeIvy'") + assert output.contains("'groovy.grape.maven.GrapeMaven' configured via -Dgroovy.grape.impl") + } +} + + + diff --git a/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeDefaultProviderSelectionTest.groovy b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeDefaultProviderSelectionTest.groovy new file mode 100644 index 00000000000..ce5514d1ea1 --- /dev/null +++ b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeDefaultProviderSelectionTest.groovy @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape + +import org.junit.jupiter.api.Test + +final class GrapeDefaultProviderSelectionTest { + @Test + void testNoConfiguredImplementationUsesDefaultAndLogsIgnoredProvider() { + String output = GrapeSelectionTestSupport.captureStderr { + assert Grape.instance != null + } + + assert output.contains("ignoring provider 'groovy.grape.maven.GrapeMaven'") + assert output.contains("in favour of default 'groovy.grape.ivy.GrapeIvy'") + } +} + diff --git a/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeImplementationSelectionTest.groovy b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeImplementationSelectionTest.groovy new file mode 100644 index 00000000000..99b36f05a12 --- /dev/null +++ b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeImplementationSelectionTest.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape + +import org.junit.jupiter.api.Test + +final class GrapeImplementationSelectionTest { + @Test + void testConfiguredImplementationMismatchSkipsServiceLoad() { + // This module runs with both implementations available; selecting an implementation + // that is not on this test runtime classpath should disable Grapes. + System.setProperty('groovy.grape.impl', 'groovy.grape.nonexistent.DoesNotExist') + String output = GrapeSelectionTestSupport.captureStderr { + assert Grape.instance == null + } + + assert output.contains("configured implementation 'groovy.grape.nonexistent.DoesNotExist' not found") + assert output.contains('Grapes disabled') + } +} diff --git a/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeSelectionTestSupport.groovy b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeSelectionTestSupport.groovy new file mode 100644 index 00000000000..d7e6ce0821e --- /dev/null +++ b/subprojects/groovy-grape-test/src/test/groovy/groovy/grape/GrapeSelectionTestSupport.groovy @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package groovy.grape + +final class GrapeSelectionTestSupport { + private GrapeSelectionTestSupport() { + } + + static String captureStderr(Closure action) { + def originalErr = System.err + def errBytes = new ByteArrayOutputStream() + try { + System.setErr(new PrintStream(errBytes, true, 'UTF-8')) + action.call() + return errBytes.toString('UTF-8') + } finally { + System.setErr(originalErr) + } + } +} diff --git a/subprojects/groovy-jmx/build.gradle b/subprojects/groovy-jmx/build.gradle index 036d27296ca..67d220ae568 100644 --- a/subprojects/groovy-jmx/build.gradle +++ b/subprojects/groovy-jmx/build.gradle @@ -29,5 +29,6 @@ dependencies { requireCapability 'org.apache.groovy:groovy-grapes' } } + testRuntimeOnly projects.groovyGrapeIvy testRuntimeOnly projects.groovySwing } diff --git a/subprojects/groovy-json/build.gradle b/subprojects/groovy-json/build.gradle index 4630a335002..15ac68ccab0 100644 --- a/subprojects/groovy-json/build.gradle +++ b/subprojects/groovy-json/build.gradle @@ -33,6 +33,7 @@ dependencies { requireCapability 'org.apache.groovy:groovy-grapes' } } + testRuntimeOnly projects.groovyGrapeIvy testRuntimeOnly projects.groovyAnt // for JavadocAssertionTests testRuntimeOnly 'com.google.code.gson:gson:2.13.2' // json-unit requires gson, jackson1 or jackson2 } diff --git a/subprojects/groovy-sql/build.gradle b/subprojects/groovy-sql/build.gradle index 116128481db..c393a07efa3 100644 --- a/subprojects/groovy-sql/build.gradle +++ b/subprojects/groovy-sql/build.gradle @@ -33,6 +33,7 @@ dependencies { requireCapability("org.apache.groovy:groovy-grapes") } } + testRuntimeOnly projects.groovyGrapeIvy testCompileOnly(projects.groovyMacro) { because "tests are using macro methods" } diff --git a/subprojects/groovy-xml/build.gradle b/subprojects/groovy-xml/build.gradle index 2869f9f6668..b7a96b8a5b2 100644 --- a/subprojects/groovy-xml/build.gradle +++ b/subprojects/groovy-xml/build.gradle @@ -36,6 +36,7 @@ dependencies { requireCapability 'org.apache.groovy:groovy-grapes' } } + testRuntimeOnly projects.groovyGrapeIvy } plugins.withId('eclipse') { diff --git a/subprojects/groovy-xml/src/main/groovy/groovy/xml/StaxBuilder.groovy b/subprojects/groovy-xml/src/main/groovy/groovy/xml/StaxBuilder.groovy index 957ad455489..05d92d58cd4 100644 --- a/subprojects/groovy-xml/src/main/groovy/groovy/xml/StaxBuilder.groovy +++ b/subprojects/groovy-xml/src/main/groovy/groovy/xml/StaxBuilder.groovy @@ -31,7 +31,10 @@ package groovy.xml * elem2('hello2') * elem3(x:7) * } - * assert writer.toString() == 'hello1hello2' + * def pretty= writer.toString() + * .replaceAll(/<\?xml[^>]*>/, '') // remove XML declaration + * assert pretty in ['hello1hello2', + * 'hello1hello2'] * * Or an external library such as Jettison can be used as follows: *
diff --git a/subprojects/groovy-xml/src/spec/test/StaxBuilderTest.groovy b/subprojects/groovy-xml/src/spec/test/StaxBuilderTest.groovy
index 35e3f0d969a..2c8968415b8 100644
--- a/subprojects/groovy-xml/src/spec/test/StaxBuilderTest.groovy
+++ b/subprojects/groovy-xml/src/spec/test/StaxBuilderTest.groovy
@@ -32,7 +32,9 @@ class StaxBuilderTest extends GroovyTestCase {
             elem2('world')
         }
 
-        assert writer.toString() == 'helloworld'
+        def pretty= writer.toString()
+            .replaceAll(/<\?xml[^>]*>/, '') // remove XML declaration
+        assert pretty == 'helloworld'
         // end::stax_builder[]
     }
 
@@ -56,4 +58,4 @@ class StaxBuilderTest extends GroovyTestCase {
             // end::stax_builder_external_library[]
         '''
     }
-}
\ No newline at end of file
+}
diff --git a/versions.properties b/versions.properties
index dcc2277ac8d..c979e2a1a16 100644
--- a/versions.properties
+++ b/versions.properties
@@ -29,7 +29,7 @@ commonsCli=1.11.0
 commonsMath3=3.6.1
 gpars=1.2.1
 ivy=2.5.3
-jackson=2.21.0
+jackson=2.21.1
 jacksonAnnotations=2.21
 jakartaServerPagesApi=3.1.1
 jakartaServletApi=6.0.0
@@ -48,6 +48,8 @@ junit5Platform=1.14.3
 log4j=1.2.17
 log4j2=2.25.3
 logback=1.5.27
+mavenResolverProvider=4.0.0-rc-5
+mavenResolverSupplier=2.0.16
 openbeans=1.0.2
 picocli=4.7.7
 qdox=2.2.0