[ 
https://issues.apache.org/jira/browse/GROOVY-11871?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18064290#comment-18064290
 ] 

ASF GitHub Bot commented on GROOVY-11871:
-----------------------------------------

Copilot commented on code in PR #2140:
URL: https://github.com/apache/groovy/pull/2140#discussion_r2908783383


##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ *  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.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.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.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations 
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+    private static final List<String> DEFAULT_CONF = 
Collections.singletonList('default')
+    private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS = 
processGrabArgs([
+            ['group', 'groupId', 'organisation', 'organization', 'org'],
+            ['module', 'artifactId', 'artifact'],
+            ['version', 'revision', 'rev'],
+            ['conf', 'scope', 'configuration'],
+    ])
+
+    @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+    private static Map<String, Set<String>> processGrabArgs(List<List<String>> 
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<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+    /** Stores the MavenGrabRecord(s) for all dependencies in each grab() 
call. */
+    final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as 
LinkedHashSet
+    boolean enableGrapes = true
+    Set<String> downloadedArtifacts = []
+    Set<String> resolvedDependencies = []
+    final List<RemoteRepository> repos = [
+        new RemoteRepository.Builder("central", "default", 
"https://repo.maven.apache.org/maven2/";).build()
+    ]
+
+    @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.processCategoryMethods(loader, file)
+                Collection<String> 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<MavenGrabRecord> grabRecordsForCurrLoader = 
getLoadedDepsForLoader(loader)
+            grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+            grabRecordsForCurrDependencies.clear()
+
+            if (args.noExceptions) {
+                return e
+            }
+            throw e
+        }
+        null
+    }
+
+
+    @Override
+    @CompileDynamic
+    Map<String, Map<String, List<String>>> enumerateGrapes() {
+        Map<String, Map<String, List<String>>> 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<String, List<String>> modules = 
bunches.computeIfAbsent(groupKey) { [:] }
+            List<String> versions = modules.computeIfAbsent(module) { [] }
+            if (!versions.contains(version)) {
+                versions << version
+            }
+        }
+
+        bunches.values()*.values()*.each { List<String> 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<MavenGrabRecord> 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<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+        List<MavenGrabRecord> 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 e
+        }
+    }
+
+    URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord... 
grabRecords) {
+        try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+            def localRepo = new LocalRepository(getGrapeCacheDir())
+            String checksumPolicy = args.disableChecksums ?
+                RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+                RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of 
FAIL for better compatibility
+
+            try (RepositorySystemSession.CloseableSession session = system
+                .createSessionBuilder()
+                .withLocalRepositories(localRepo)
+                .setChecksumPolicy(checksumPolicy)
+                .setSystemProperty('aether.artifactDescriptor.ignoreErrors', 
'true')
+                
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
 'true')
+                .build()) {
+
+                List<URI> results = []
+
+                for (MavenGrabRecord grabRecord : grabRecords) {
+                    String coords = 
"${grabRecord.groupId()}:${grabRecord.module()}"
+                    if (grabRecord.classifier()) {
+                        coords += ":${grabRecord.classifier()}"
+                    }
+                    coords += ":${grabRecord.ext()}:${grabRecord.version()}"

Review Comment:
   The coordinate string is constructed incorrectly when a classifier is 
present. Maven Resolver's `DefaultArtifact` expects the 5-part format 
`groupId:artifactId:extension:classifier:version`, but this code produces 
`groupId:artifactId:classifier:extension:version` (classifier and extension are 
swapped).
   
   The classifier should be appended after the extension, not before it. The 
code should build the coords as 
`"${groupId}:${module}:${ext}:${classifier}:${version}"` when a classifier is 
present.



##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ *  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.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.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.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations 
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+    private static final List<String> DEFAULT_CONF = 
Collections.singletonList('default')
+    private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS = 
processGrabArgs([
+            ['group', 'groupId', 'organisation', 'organization', 'org'],
+            ['module', 'artifactId', 'artifact'],
+            ['version', 'revision', 'rev'],
+            ['conf', 'scope', 'configuration'],
+    ])
+
+    @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+    private static Map<String, Set<String>> processGrabArgs(List<List<String>> 
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<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+    /** Stores the MavenGrabRecord(s) for all dependencies in each grab() 
call. */
+    final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as 
LinkedHashSet
+    boolean enableGrapes = true
+    Set<String> downloadedArtifacts = []
+    Set<String> resolvedDependencies = []
+    final List<RemoteRepository> repos = [
+        new RemoteRepository.Builder("central", "default", 
"https://repo.maven.apache.org/maven2/";).build()
+    ]
+
+    @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.processCategoryMethods(loader, file)
+                Collection<String> 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<MavenGrabRecord> grabRecordsForCurrLoader = 
getLoadedDepsForLoader(loader)
+            grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+            grabRecordsForCurrDependencies.clear()
+
+            if (args.noExceptions) {
+                return e
+            }
+            throw e
+        }
+        null
+    }
+
+
+    @Override
+    @CompileDynamic
+    Map<String, Map<String, List<String>>> enumerateGrapes() {
+        Map<String, Map<String, List<String>>> 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<String, List<String>> modules = 
bunches.computeIfAbsent(groupKey) { [:] }
+            List<String> versions = modules.computeIfAbsent(module) { [] }
+            if (!versions.contains(version)) {
+                versions << version
+            }
+        }
+
+        bunches.values()*.values()*.each { List<String> 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<MavenGrabRecord> 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<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+        List<MavenGrabRecord> 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 e
+        }
+    }
+
+    URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord... 
grabRecords) {
+        try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+            def localRepo = new LocalRepository(getGrapeCacheDir())
+            String checksumPolicy = args.disableChecksums ?
+                RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+                RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of 
FAIL for better compatibility
+
+            try (RepositorySystemSession.CloseableSession session = system
+                .createSessionBuilder()
+                .withLocalRepositories(localRepo)
+                .setChecksumPolicy(checksumPolicy)
+                .setSystemProperty('aether.artifactDescriptor.ignoreErrors', 
'true')
+                
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
 'true')
+                .build()) {
+
+                List<URI> results = []
+
+                for (MavenGrabRecord grabRecord : grabRecords) {
+                    String coords = 
"${grabRecord.groupId()}:${grabRecord.module()}"
+                    if (grabRecord.classifier()) {
+                        coords += ":${grabRecord.classifier()}"
+                    }
+                    coords += ":${grabRecord.ext()}:${grabRecord.version()}"
+
+                    Artifact artifact = new DefaultArtifact(coords)
+
+                    String scope = grabRecord.conf()?.get(0) ?: 
JavaScopes.COMPILE
+                    if (scope == 'default') scope = JavaScopes.COMPILE
+
+                    List<ArtifactResult> artifactResults
+                    if (grabRecord.transitive()) {
+                        DependencyFilter classpathFilter = 
DependencyFilterUtils.classpathFilter(scope)
+                        CollectRequest collectRequest = new CollectRequest(
+                            root: new Dependency(artifact, scope),
+                            repositories: repos
+                        )
+                        DependencyRequest dependencyRequest = new 
DependencyRequest(collectRequest, classpathFilter)
+                        artifactResults = system.resolveDependencies(session, 
dependencyRequest).getArtifactResults()
+                    } else {
+                        ArtifactRequest artifactRequest = new 
ArtifactRequest(artifact, repos, null)
+                        artifactResults = [system.resolveArtifact(session, 
artifactRequest)]
+                    }
+
+                    for (ArtifactResult found : artifactResults) {
+                        if (found.artifact.file) {
+                            results << found.artifact.file.toURI()
+
+                            if (depsInfo != null) {
+                                depsInfo << [
+                                    'group': found.artifact.groupId,
+                                    'module': found.artifact.artifactId,
+                                    'version': found.artifact.version
+                                ]

Review Comment:
   The `depsInfo` map entries use the key `'version'` here, but 
`GrapeMain.Resolve` (line 299 of `GrapeMain.groovy`) accesses `dep.revision` 
for the Ivy-format output (`-i` flag). The existing `GrapeIvy` implementation 
uses `'revision'` as the key (see `GrapeIvy.groovy:551`). This should use 
`'revision'` instead of `'version'` to maintain compatibility with the existing 
`GrapeMain` consumer, or both keys could be included.



##########
src/main/java/groovy/grape/GrapeEngine.java:
##########
@@ -43,5 +43,11 @@ public interface GrapeEngine {
     Map[] listDependencies(ClassLoader classLoader);
 
     void addResolver(Map<String, Object> 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)
+     */
+    void setLoggingLevel(int level);

Review Comment:
   Adding `setLoggingLevel(int level)` as an abstract method to the 
`GrapeEngine` interface is a breaking change for any external implementations 
of this interface. Since this is a public API, consider providing a default 
(no-op) implementation to maintain backward compatibility:
   
   ```java
   default void setLoggingLevel(int level) { }
   ```
   
   This way, existing third-party `GrapeEngine` implementations won't be forced 
to update.



##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ *  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.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.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.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations 
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+    private static final List<String> DEFAULT_CONF = 
Collections.singletonList('default')
+    private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS = 
processGrabArgs([
+            ['group', 'groupId', 'organisation', 'organization', 'org'],
+            ['module', 'artifactId', 'artifact'],
+            ['version', 'revision', 'rev'],
+            ['conf', 'scope', 'configuration'],
+    ])
+
+    @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+    private static Map<String, Set<String>> processGrabArgs(List<List<String>> 
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<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+    /** Stores the MavenGrabRecord(s) for all dependencies in each grab() 
call. */
+    final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as 
LinkedHashSet
+    boolean enableGrapes = true
+    Set<String> downloadedArtifacts = []
+    Set<String> resolvedDependencies = []
+    final List<RemoteRepository> repos = [
+        new RemoteRepository.Builder("central", "default", 
"https://repo.maven.apache.org/maven2/";).build()
+    ]
+
+    @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.processCategoryMethods(loader, file)
+                Collection<String> 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<MavenGrabRecord> grabRecordsForCurrLoader = 
getLoadedDepsForLoader(loader)
+            grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+            grabRecordsForCurrDependencies.clear()
+
+            if (args.noExceptions) {
+                return e
+            }
+            throw e
+        }
+        null
+    }
+
+
+    @Override
+    @CompileDynamic
+    Map<String, Map<String, List<String>>> enumerateGrapes() {
+        Map<String, Map<String, List<String>>> 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<String, List<String>> modules = 
bunches.computeIfAbsent(groupKey) { [:] }
+            List<String> versions = modules.computeIfAbsent(module) { [] }
+            if (!versions.contains(version)) {
+                versions << version
+            }
+        }
+
+        bunches.values()*.values()*.each { List<String> 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<MavenGrabRecord> 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<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+        List<MavenGrabRecord> 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 e
+        }
+    }
+
+    URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord... 
grabRecords) {
+        try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+            def localRepo = new LocalRepository(getGrapeCacheDir())
+            String checksumPolicy = args.disableChecksums ?
+                RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+                RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of 
FAIL for better compatibility
+
+            try (RepositorySystemSession.CloseableSession session = system
+                .createSessionBuilder()
+                .withLocalRepositories(localRepo)
+                .setChecksumPolicy(checksumPolicy)
+                .setSystemProperty('aether.artifactDescriptor.ignoreErrors', 
'true')
+                
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
 'true')
+                .build()) {
+
+                List<URI> results = []
+
+                for (MavenGrabRecord grabRecord : grabRecords) {
+                    String coords = 
"${grabRecord.groupId()}:${grabRecord.module()}"
+                    if (grabRecord.classifier()) {
+                        coords += ":${grabRecord.classifier()}"
+                    }
+                    coords += ":${grabRecord.ext()}:${grabRecord.version()}"
+
+                    Artifact artifact = new DefaultArtifact(coords)
+
+                    String scope = grabRecord.conf()?.get(0) ?: 
JavaScopes.COMPILE
+                    if (scope == 'default') scope = JavaScopes.COMPILE
+
+                    List<ArtifactResult> artifactResults
+                    if (grabRecord.transitive()) {
+                        DependencyFilter classpathFilter = 
DependencyFilterUtils.classpathFilter(scope)
+                        CollectRequest collectRequest = new CollectRequest(
+                            root: new Dependency(artifact, scope),
+                            repositories: repos
+                        )
+                        DependencyRequest dependencyRequest = new 
DependencyRequest(collectRequest, classpathFilter)
+                        artifactResults = system.resolveDependencies(session, 
dependencyRequest).getArtifactResults()
+                    } else {
+                        ArtifactRequest artifactRequest = new 
ArtifactRequest(artifact, repos, null)
+                        artifactResults = [system.resolveArtifact(session, 
artifactRequest)]
+                    }
+
+                    for (ArtifactResult found : artifactResults) {
+                        if (found.artifact.file) {
+                            results << found.artifact.file.toURI()
+
+                            if (depsInfo != null) {
+                                depsInfo << [
+                                    'group': found.artifact.groupId,
+                                    'module': found.artifact.artifactId,
+                                    'version': found.artifact.version
+                                ]
+                            }
+                        }
+                    }
+                }
+
+                return results as URI[]
+            }
+        }
+    }
+
+    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 / \\ : \" < > |")

Review Comment:
   The version validation regex `'[^/:"<>|]*'` is missing the backslash 
character compared to the original `GrapeIvy` implementation which uses 
`'[^\\\\/:"<>|]*'` (at `GrapeIvy.groovy:201`). The error message at line 311 
mentions `\\` but the regex doesn't actually reject it. The regex should be 
`'[^\\\\/:"<>|]*'` to match the GrapeIvy behavior.



##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ *  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.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.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.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations 
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+    private static final List<String> DEFAULT_CONF = 
Collections.singletonList('default')
+    private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS = 
processGrabArgs([
+            ['group', 'groupId', 'organisation', 'organization', 'org'],
+            ['module', 'artifactId', 'artifact'],
+            ['version', 'revision', 'rev'],
+            ['conf', 'scope', 'configuration'],
+    ])
+
+    @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+    private static Map<String, Set<String>> processGrabArgs(List<List<String>> 
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<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+    /** Stores the MavenGrabRecord(s) for all dependencies in each grab() 
call. */
+    final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as 
LinkedHashSet
+    boolean enableGrapes = true
+    Set<String> downloadedArtifacts = []
+    Set<String> resolvedDependencies = []
+    final List<RemoteRepository> repos = [
+        new RemoteRepository.Builder("central", "default", 
"https://repo.maven.apache.org/maven2/";).build()
+    ]
+
+    @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.processCategoryMethods(loader, file)
+                Collection<String> 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<MavenGrabRecord> grabRecordsForCurrLoader = 
getLoadedDepsForLoader(loader)
+            grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+            grabRecordsForCurrDependencies.clear()
+
+            if (args.noExceptions) {
+                return e
+            }
+            throw e
+        }
+        null
+    }
+
+
+    @Override
+    @CompileDynamic
+    Map<String, Map<String, List<String>>> enumerateGrapes() {
+        Map<String, Map<String, List<String>>> 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<String, List<String>> modules = 
bunches.computeIfAbsent(groupKey) { [:] }
+            List<String> versions = modules.computeIfAbsent(module) { [] }
+            if (!versions.contains(version)) {
+                versions << version
+            }
+        }
+
+        bunches.values()*.values()*.each { List<String> 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<MavenGrabRecord> 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<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+        List<MavenGrabRecord> 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 e
+        }
+    }
+
+    URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord... 
grabRecords) {
+        try (RepositorySystem system = new RepositorySystemSupplier().get()) {

Review Comment:
   A new `RepositorySystemSupplier().get()` (and hence a new 
`RepositorySystem`) is created on every call to `getDependencies`. The 
`RepositorySystemSupplier` bootstraps a significant amount of infrastructure 
(service locator, transport providers, etc.). Consider creating the 
`RepositorySystem` once (e.g., in the constructor or lazily) and reusing it 
across calls, similar to how `GrapeIvy` creates its `Ivy` instance once in its 
constructor.





> Support Maven Resolver based version of Grapes
> ----------------------------------------------
>
>                 Key: GROOVY-11871
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11871
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Priority: Major
>




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to