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.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to