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]
