[
https://issues.apache.org/jira/browse/GROOVY-11871?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18064290#comment-18064290
]
ASF GitHub Bot commented on GROOVY-11871:
-----------------------------------------
Copilot commented on code in PR #2140:
URL: https://github.com/apache/groovy/pull/2140#discussion_r2908783383
##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.grape.maven
+
+import groovy.grape.GrapeEngine
+import groovy.grape.GrapeUtil
+import groovy.transform.AutoFinal
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.NamedParam
+import groovy.transform.NamedParams
+import org.codehaus.groovy.reflection.ReflectionUtils
+import org.eclipse.aether.RepositorySystem
+import org.eclipse.aether.RepositorySystemSession
+import org.eclipse.aether.artifact.Artifact
+import org.eclipse.aether.artifact.DefaultArtifact
+import org.eclipse.aether.collection.CollectRequest
+import org.eclipse.aether.graph.Dependency
+import org.eclipse.aether.graph.DependencyFilter
+import org.eclipse.aether.repository.LocalRepository
+import org.eclipse.aether.repository.RemoteRepository
+import org.eclipse.aether.repository.RepositoryPolicy
+import org.eclipse.aether.resolution.ArtifactRequest
+import org.eclipse.aether.resolution.ArtifactResult
+import org.eclipse.aether.resolution.DependencyRequest
+import org.eclipse.aether.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+ private static final List<String> DEFAULT_CONF =
Collections.singletonList('default')
+ private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS =
processGrabArgs([
+ ['group', 'groupId', 'organisation', 'organization', 'org'],
+ ['module', 'artifactId', 'artifact'],
+ ['version', 'revision', 'rev'],
+ ['conf', 'scope', 'configuration'],
+ ])
+
+ @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+ private static Map<String, Set<String>> processGrabArgs(List<List<String>>
grabArgs) {
+ grabArgs.inject([:]) { Map m, List g -> g.each { a -> m[a] = (g - a)
as Set }; m }
+ }
+
+ // weak hash map so we don't leak loaders directly
+ final Map<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+ /** Stores the MavenGrabRecord(s) for all dependencies in each grab()
call. */
+ final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as
LinkedHashSet
+ boolean enableGrapes = true
+ Set<String> downloadedArtifacts = []
+ Set<String> resolvedDependencies = []
+ final List<RemoteRepository> repos = [
+ new RemoteRepository.Builder("central", "default",
"https://repo.maven.apache.org/maven2/").build()
+ ]
+
+ @Override
+ grab(String endorsedModule) {
+ grab(group: 'groovy.endorsed', module: endorsedModule, version:
GroovySystem.getVersion())
+ }
+
+ @Override
+ grab(Map args) {
+ args.calleeDepth = args.calleeDepth ?: DEFAULT_CALLEE_DEPTH + 1
+ grab(args, args)
+ }
+
+ @Override
+ grab(Map args, Map... dependencies) {
+ ClassLoader loader = null
+ grabRecordsForCurrDependencies.clear()
+
+ try {
+ // identify the target classloader early, so we fail before
checking repositories
+ loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an
exception
+ if (!loader) return
+
+ URI[] uris = resolve(loader, args, dependencies)
+ for (URI uri : uris) {
+ GrapeUtil.addURL(loader, uri)
+ }
+ boolean runnerServicesFound = false
+ for (URI uri : uris) {
+ // TODO: check artifact type, jar vs library, etc.
+ File file = new File(uri)
+ GrapeUtil.processCategoryMethods(loader, file)
+ Collection<String> services =
GrapeUtil.processMetaInfServices(loader, file)
+ if (!runnerServicesFound) {
+ runnerServicesFound = GrapeUtil.checkForRunner(services)
+ }
+ }
+ if (runnerServicesFound) {
+ GrapeUtil.registryLoad(loader)
+ }
+ } catch (Exception e) {
+ // clean-up the state first
+ Set<MavenGrabRecord> grabRecordsForCurrLoader =
getLoadedDepsForLoader(loader)
+ grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+
+ if (args.noExceptions) {
+ return e
+ }
+ throw e
+ }
+ null
+ }
+
+
+ @Override
+ @CompileDynamic
+ Map<String, Map<String, List<String>>> enumerateGrapes() {
+ Map<String, Map<String, List<String>>> bunches = [:]
+ File cacheRoot = grapeCacheDir.canonicalFile
+
+ cacheRoot.eachFileRecurse { File f ->
+ if (!f.isFile()) return
+ String name = f.name
+ if (name.endsWith('.sha1') || name.endsWith('.md5') ||
name.endsWith('.asc')) return
+
+ File versionDir = f.parentFile
+ File moduleDir = versionDir?.parentFile
+ File groupDir = moduleDir?.parentFile
+ if (!versionDir || !moduleDir || !groupDir) return
+
+ String version = versionDir.name
+ String module = moduleDir.name
+ String expectedPrefix = module + '-' + version
+ if (!name.startsWith(expectedPrefix)) return
+
+ String groupPath =
cacheRoot.toPath().relativize(groupDir.toPath()).toString()
+ if (!groupPath) return
+ String groupKey = groupPath.replace(File.separatorChar, '.' as
char)
+
+ Map<String, List<String>> modules =
bunches.computeIfAbsent(groupKey) { [:] }
+ List<String> versions = modules.computeIfAbsent(module) { [] }
+ if (!versions.contains(version)) {
+ versions << version
+ }
+ }
+
+ bunches.values()*.values()*.each { List<String> versions ->
versions.sort() }
+ bunches
+ }
+
+ void uninstallArtifact(String group, String module, String rev) {
+ String groupPath = group.replace('.' as char, File.separatorChar)
+ def artifactDir = new File(grapeCacheDir, groupPath + File.separator +
module + File.separator + rev)
+ if (artifactDir.exists()) {
+ artifactDir.deleteDir()
+ }
+ }
+
+ @Override
+ URI[] resolve(Map args, Map... dependencies) {
+ resolve(args, null, dependencies)
+ }
+
+ @Override
+ URI[] resolve(Map args, List depsInfo, Map... dependencies) {
+ // identify the target classloader early, so we fail before checking
repositories
+ ClassLoader loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an exception
+ if (!loader) {
+ return new URI[0]
+ }
+
+ resolve(loader, args, depsInfo, dependencies)
+ }
+
+ private Set<MavenGrabRecord> getLoadedDepsForLoader(ClassLoader loader) {
+ // use a LinkedHashSet to preserve the initial insertion order
+ loadedDeps.computeIfAbsent(loader, k -> [] as LinkedHashSet)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, Map... dependencies) {
+ resolve(loader, args, null, dependencies)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, List depsInfo, Map...
dependencies) {
+ if (!enableGrapes) {
+ return new URI[0]
+ }
+
+ boolean populateDepsInfo = (depsInfo != null)
+ Set<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+ List<MavenGrabRecord> grabRecords = []
+ for (Map dep : dependencies) {
+ MavenGrabRecord mgr = createGrabRecord(dep)
+ grabRecordsForCurrDependencies.add(mgr)
+ localDeps.add(mgr)
+ grabRecords.add(mgr)
+ }
+
+ try {
+ URI[] results = getDependencies(args, populateDepsInfo ? depsInfo
: null, grabRecords as MavenGrabRecord[])
+ return results
+ } catch (Exception e) {
+ localDeps.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+ throw e
+ }
+ }
+
+ URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord...
grabRecords) {
+ try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+ def localRepo = new LocalRepository(getGrapeCacheDir())
+ String checksumPolicy = args.disableChecksums ?
+ RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+ RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of
FAIL for better compatibility
+
+ try (RepositorySystemSession.CloseableSession session = system
+ .createSessionBuilder()
+ .withLocalRepositories(localRepo)
+ .setChecksumPolicy(checksumPolicy)
+ .setSystemProperty('aether.artifactDescriptor.ignoreErrors',
'true')
+
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
'true')
+ .build()) {
+
+ List<URI> results = []
+
+ for (MavenGrabRecord grabRecord : grabRecords) {
+ String coords =
"${grabRecord.groupId()}:${grabRecord.module()}"
+ if (grabRecord.classifier()) {
+ coords += ":${grabRecord.classifier()}"
+ }
+ coords += ":${grabRecord.ext()}:${grabRecord.version()}"
Review Comment:
The coordinate string is constructed incorrectly when a classifier is
present. Maven Resolver's `DefaultArtifact` expects the 5-part format
`groupId:artifactId:extension:classifier:version`, but this code produces
`groupId:artifactId:classifier:extension:version` (classifier and extension are
swapped).
The classifier should be appended after the extension, not before it. The
code should build the coords as
`"${groupId}:${module}:${ext}:${classifier}:${version}"` when a classifier is
present.
##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.grape.maven
+
+import groovy.grape.GrapeEngine
+import groovy.grape.GrapeUtil
+import groovy.transform.AutoFinal
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.NamedParam
+import groovy.transform.NamedParams
+import org.codehaus.groovy.reflection.ReflectionUtils
+import org.eclipse.aether.RepositorySystem
+import org.eclipse.aether.RepositorySystemSession
+import org.eclipse.aether.artifact.Artifact
+import org.eclipse.aether.artifact.DefaultArtifact
+import org.eclipse.aether.collection.CollectRequest
+import org.eclipse.aether.graph.Dependency
+import org.eclipse.aether.graph.DependencyFilter
+import org.eclipse.aether.repository.LocalRepository
+import org.eclipse.aether.repository.RemoteRepository
+import org.eclipse.aether.repository.RepositoryPolicy
+import org.eclipse.aether.resolution.ArtifactRequest
+import org.eclipse.aether.resolution.ArtifactResult
+import org.eclipse.aether.resolution.DependencyRequest
+import org.eclipse.aether.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+ private static final List<String> DEFAULT_CONF =
Collections.singletonList('default')
+ private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS =
processGrabArgs([
+ ['group', 'groupId', 'organisation', 'organization', 'org'],
+ ['module', 'artifactId', 'artifact'],
+ ['version', 'revision', 'rev'],
+ ['conf', 'scope', 'configuration'],
+ ])
+
+ @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+ private static Map<String, Set<String>> processGrabArgs(List<List<String>>
grabArgs) {
+ grabArgs.inject([:]) { Map m, List g -> g.each { a -> m[a] = (g - a)
as Set }; m }
+ }
+
+ // weak hash map so we don't leak loaders directly
+ final Map<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+ /** Stores the MavenGrabRecord(s) for all dependencies in each grab()
call. */
+ final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as
LinkedHashSet
+ boolean enableGrapes = true
+ Set<String> downloadedArtifacts = []
+ Set<String> resolvedDependencies = []
+ final List<RemoteRepository> repos = [
+ new RemoteRepository.Builder("central", "default",
"https://repo.maven.apache.org/maven2/").build()
+ ]
+
+ @Override
+ grab(String endorsedModule) {
+ grab(group: 'groovy.endorsed', module: endorsedModule, version:
GroovySystem.getVersion())
+ }
+
+ @Override
+ grab(Map args) {
+ args.calleeDepth = args.calleeDepth ?: DEFAULT_CALLEE_DEPTH + 1
+ grab(args, args)
+ }
+
+ @Override
+ grab(Map args, Map... dependencies) {
+ ClassLoader loader = null
+ grabRecordsForCurrDependencies.clear()
+
+ try {
+ // identify the target classloader early, so we fail before
checking repositories
+ loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an
exception
+ if (!loader) return
+
+ URI[] uris = resolve(loader, args, dependencies)
+ for (URI uri : uris) {
+ GrapeUtil.addURL(loader, uri)
+ }
+ boolean runnerServicesFound = false
+ for (URI uri : uris) {
+ // TODO: check artifact type, jar vs library, etc.
+ File file = new File(uri)
+ GrapeUtil.processCategoryMethods(loader, file)
+ Collection<String> services =
GrapeUtil.processMetaInfServices(loader, file)
+ if (!runnerServicesFound) {
+ runnerServicesFound = GrapeUtil.checkForRunner(services)
+ }
+ }
+ if (runnerServicesFound) {
+ GrapeUtil.registryLoad(loader)
+ }
+ } catch (Exception e) {
+ // clean-up the state first
+ Set<MavenGrabRecord> grabRecordsForCurrLoader =
getLoadedDepsForLoader(loader)
+ grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+
+ if (args.noExceptions) {
+ return e
+ }
+ throw e
+ }
+ null
+ }
+
+
+ @Override
+ @CompileDynamic
+ Map<String, Map<String, List<String>>> enumerateGrapes() {
+ Map<String, Map<String, List<String>>> bunches = [:]
+ File cacheRoot = grapeCacheDir.canonicalFile
+
+ cacheRoot.eachFileRecurse { File f ->
+ if (!f.isFile()) return
+ String name = f.name
+ if (name.endsWith('.sha1') || name.endsWith('.md5') ||
name.endsWith('.asc')) return
+
+ File versionDir = f.parentFile
+ File moduleDir = versionDir?.parentFile
+ File groupDir = moduleDir?.parentFile
+ if (!versionDir || !moduleDir || !groupDir) return
+
+ String version = versionDir.name
+ String module = moduleDir.name
+ String expectedPrefix = module + '-' + version
+ if (!name.startsWith(expectedPrefix)) return
+
+ String groupPath =
cacheRoot.toPath().relativize(groupDir.toPath()).toString()
+ if (!groupPath) return
+ String groupKey = groupPath.replace(File.separatorChar, '.' as
char)
+
+ Map<String, List<String>> modules =
bunches.computeIfAbsent(groupKey) { [:] }
+ List<String> versions = modules.computeIfAbsent(module) { [] }
+ if (!versions.contains(version)) {
+ versions << version
+ }
+ }
+
+ bunches.values()*.values()*.each { List<String> versions ->
versions.sort() }
+ bunches
+ }
+
+ void uninstallArtifact(String group, String module, String rev) {
+ String groupPath = group.replace('.' as char, File.separatorChar)
+ def artifactDir = new File(grapeCacheDir, groupPath + File.separator +
module + File.separator + rev)
+ if (artifactDir.exists()) {
+ artifactDir.deleteDir()
+ }
+ }
+
+ @Override
+ URI[] resolve(Map args, Map... dependencies) {
+ resolve(args, null, dependencies)
+ }
+
+ @Override
+ URI[] resolve(Map args, List depsInfo, Map... dependencies) {
+ // identify the target classloader early, so we fail before checking
repositories
+ ClassLoader loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an exception
+ if (!loader) {
+ return new URI[0]
+ }
+
+ resolve(loader, args, depsInfo, dependencies)
+ }
+
+ private Set<MavenGrabRecord> getLoadedDepsForLoader(ClassLoader loader) {
+ // use a LinkedHashSet to preserve the initial insertion order
+ loadedDeps.computeIfAbsent(loader, k -> [] as LinkedHashSet)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, Map... dependencies) {
+ resolve(loader, args, null, dependencies)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, List depsInfo, Map...
dependencies) {
+ if (!enableGrapes) {
+ return new URI[0]
+ }
+
+ boolean populateDepsInfo = (depsInfo != null)
+ Set<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+ List<MavenGrabRecord> grabRecords = []
+ for (Map dep : dependencies) {
+ MavenGrabRecord mgr = createGrabRecord(dep)
+ grabRecordsForCurrDependencies.add(mgr)
+ localDeps.add(mgr)
+ grabRecords.add(mgr)
+ }
+
+ try {
+ URI[] results = getDependencies(args, populateDepsInfo ? depsInfo
: null, grabRecords as MavenGrabRecord[])
+ return results
+ } catch (Exception e) {
+ localDeps.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+ throw e
+ }
+ }
+
+ URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord...
grabRecords) {
+ try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+ def localRepo = new LocalRepository(getGrapeCacheDir())
+ String checksumPolicy = args.disableChecksums ?
+ RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+ RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of
FAIL for better compatibility
+
+ try (RepositorySystemSession.CloseableSession session = system
+ .createSessionBuilder()
+ .withLocalRepositories(localRepo)
+ .setChecksumPolicy(checksumPolicy)
+ .setSystemProperty('aether.artifactDescriptor.ignoreErrors',
'true')
+
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
'true')
+ .build()) {
+
+ List<URI> results = []
+
+ for (MavenGrabRecord grabRecord : grabRecords) {
+ String coords =
"${grabRecord.groupId()}:${grabRecord.module()}"
+ if (grabRecord.classifier()) {
+ coords += ":${grabRecord.classifier()}"
+ }
+ coords += ":${grabRecord.ext()}:${grabRecord.version()}"
+
+ Artifact artifact = new DefaultArtifact(coords)
+
+ String scope = grabRecord.conf()?.get(0) ?:
JavaScopes.COMPILE
+ if (scope == 'default') scope = JavaScopes.COMPILE
+
+ List<ArtifactResult> artifactResults
+ if (grabRecord.transitive()) {
+ DependencyFilter classpathFilter =
DependencyFilterUtils.classpathFilter(scope)
+ CollectRequest collectRequest = new CollectRequest(
+ root: new Dependency(artifact, scope),
+ repositories: repos
+ )
+ DependencyRequest dependencyRequest = new
DependencyRequest(collectRequest, classpathFilter)
+ artifactResults = system.resolveDependencies(session,
dependencyRequest).getArtifactResults()
+ } else {
+ ArtifactRequest artifactRequest = new
ArtifactRequest(artifact, repos, null)
+ artifactResults = [system.resolveArtifact(session,
artifactRequest)]
+ }
+
+ for (ArtifactResult found : artifactResults) {
+ if (found.artifact.file) {
+ results << found.artifact.file.toURI()
+
+ if (depsInfo != null) {
+ depsInfo << [
+ 'group': found.artifact.groupId,
+ 'module': found.artifact.artifactId,
+ 'version': found.artifact.version
+ ]
Review Comment:
The `depsInfo` map entries use the key `'version'` here, but
`GrapeMain.Resolve` (line 299 of `GrapeMain.groovy`) accesses `dep.revision`
for the Ivy-format output (`-i` flag). The existing `GrapeIvy` implementation
uses `'revision'` as the key (see `GrapeIvy.groovy:551`). This should use
`'revision'` instead of `'version'` to maintain compatibility with the existing
`GrapeMain` consumer, or both keys could be included.
##########
src/main/java/groovy/grape/GrapeEngine.java:
##########
@@ -43,5 +43,11 @@ public interface GrapeEngine {
Map[] listDependencies(ClassLoader classLoader);
void addResolver(Map<String, Object> args);
+
+ /**
+ * Sets the logging level for the grape engine.
+ * @param level the logging level (0=quiet/errors only, 1=warn, 2=info,
3=verbose, 4=debug)
+ */
+ void setLoggingLevel(int level);
Review Comment:
Adding `setLoggingLevel(int level)` as an abstract method to the
`GrapeEngine` interface is a breaking change for any external implementations
of this interface. Since this is a public API, consider providing a default
(no-op) implementation to maintain backward compatibility:
```java
default void setLoggingLevel(int level) { }
```
This way, existing third-party `GrapeEngine` implementations won't be forced
to update.
##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.grape.maven
+
+import groovy.grape.GrapeEngine
+import groovy.grape.GrapeUtil
+import groovy.transform.AutoFinal
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.NamedParam
+import groovy.transform.NamedParams
+import org.codehaus.groovy.reflection.ReflectionUtils
+import org.eclipse.aether.RepositorySystem
+import org.eclipse.aether.RepositorySystemSession
+import org.eclipse.aether.artifact.Artifact
+import org.eclipse.aether.artifact.DefaultArtifact
+import org.eclipse.aether.collection.CollectRequest
+import org.eclipse.aether.graph.Dependency
+import org.eclipse.aether.graph.DependencyFilter
+import org.eclipse.aether.repository.LocalRepository
+import org.eclipse.aether.repository.RemoteRepository
+import org.eclipse.aether.repository.RepositoryPolicy
+import org.eclipse.aether.resolution.ArtifactRequest
+import org.eclipse.aether.resolution.ArtifactResult
+import org.eclipse.aether.resolution.DependencyRequest
+import org.eclipse.aether.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+ private static final List<String> DEFAULT_CONF =
Collections.singletonList('default')
+ private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS =
processGrabArgs([
+ ['group', 'groupId', 'organisation', 'organization', 'org'],
+ ['module', 'artifactId', 'artifact'],
+ ['version', 'revision', 'rev'],
+ ['conf', 'scope', 'configuration'],
+ ])
+
+ @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+ private static Map<String, Set<String>> processGrabArgs(List<List<String>>
grabArgs) {
+ grabArgs.inject([:]) { Map m, List g -> g.each { a -> m[a] = (g - a)
as Set }; m }
+ }
+
+ // weak hash map so we don't leak loaders directly
+ final Map<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+ /** Stores the MavenGrabRecord(s) for all dependencies in each grab()
call. */
+ final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as
LinkedHashSet
+ boolean enableGrapes = true
+ Set<String> downloadedArtifacts = []
+ Set<String> resolvedDependencies = []
+ final List<RemoteRepository> repos = [
+ new RemoteRepository.Builder("central", "default",
"https://repo.maven.apache.org/maven2/").build()
+ ]
+
+ @Override
+ grab(String endorsedModule) {
+ grab(group: 'groovy.endorsed', module: endorsedModule, version:
GroovySystem.getVersion())
+ }
+
+ @Override
+ grab(Map args) {
+ args.calleeDepth = args.calleeDepth ?: DEFAULT_CALLEE_DEPTH + 1
+ grab(args, args)
+ }
+
+ @Override
+ grab(Map args, Map... dependencies) {
+ ClassLoader loader = null
+ grabRecordsForCurrDependencies.clear()
+
+ try {
+ // identify the target classloader early, so we fail before
checking repositories
+ loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an
exception
+ if (!loader) return
+
+ URI[] uris = resolve(loader, args, dependencies)
+ for (URI uri : uris) {
+ GrapeUtil.addURL(loader, uri)
+ }
+ boolean runnerServicesFound = false
+ for (URI uri : uris) {
+ // TODO: check artifact type, jar vs library, etc.
+ File file = new File(uri)
+ GrapeUtil.processCategoryMethods(loader, file)
+ Collection<String> services =
GrapeUtil.processMetaInfServices(loader, file)
+ if (!runnerServicesFound) {
+ runnerServicesFound = GrapeUtil.checkForRunner(services)
+ }
+ }
+ if (runnerServicesFound) {
+ GrapeUtil.registryLoad(loader)
+ }
+ } catch (Exception e) {
+ // clean-up the state first
+ Set<MavenGrabRecord> grabRecordsForCurrLoader =
getLoadedDepsForLoader(loader)
+ grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+
+ if (args.noExceptions) {
+ return e
+ }
+ throw e
+ }
+ null
+ }
+
+
+ @Override
+ @CompileDynamic
+ Map<String, Map<String, List<String>>> enumerateGrapes() {
+ Map<String, Map<String, List<String>>> bunches = [:]
+ File cacheRoot = grapeCacheDir.canonicalFile
+
+ cacheRoot.eachFileRecurse { File f ->
+ if (!f.isFile()) return
+ String name = f.name
+ if (name.endsWith('.sha1') || name.endsWith('.md5') ||
name.endsWith('.asc')) return
+
+ File versionDir = f.parentFile
+ File moduleDir = versionDir?.parentFile
+ File groupDir = moduleDir?.parentFile
+ if (!versionDir || !moduleDir || !groupDir) return
+
+ String version = versionDir.name
+ String module = moduleDir.name
+ String expectedPrefix = module + '-' + version
+ if (!name.startsWith(expectedPrefix)) return
+
+ String groupPath =
cacheRoot.toPath().relativize(groupDir.toPath()).toString()
+ if (!groupPath) return
+ String groupKey = groupPath.replace(File.separatorChar, '.' as
char)
+
+ Map<String, List<String>> modules =
bunches.computeIfAbsent(groupKey) { [:] }
+ List<String> versions = modules.computeIfAbsent(module) { [] }
+ if (!versions.contains(version)) {
+ versions << version
+ }
+ }
+
+ bunches.values()*.values()*.each { List<String> versions ->
versions.sort() }
+ bunches
+ }
+
+ void uninstallArtifact(String group, String module, String rev) {
+ String groupPath = group.replace('.' as char, File.separatorChar)
+ def artifactDir = new File(grapeCacheDir, groupPath + File.separator +
module + File.separator + rev)
+ if (artifactDir.exists()) {
+ artifactDir.deleteDir()
+ }
+ }
+
+ @Override
+ URI[] resolve(Map args, Map... dependencies) {
+ resolve(args, null, dependencies)
+ }
+
+ @Override
+ URI[] resolve(Map args, List depsInfo, Map... dependencies) {
+ // identify the target classloader early, so we fail before checking
repositories
+ ClassLoader loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an exception
+ if (!loader) {
+ return new URI[0]
+ }
+
+ resolve(loader, args, depsInfo, dependencies)
+ }
+
+ private Set<MavenGrabRecord> getLoadedDepsForLoader(ClassLoader loader) {
+ // use a LinkedHashSet to preserve the initial insertion order
+ loadedDeps.computeIfAbsent(loader, k -> [] as LinkedHashSet)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, Map... dependencies) {
+ resolve(loader, args, null, dependencies)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, List depsInfo, Map...
dependencies) {
+ if (!enableGrapes) {
+ return new URI[0]
+ }
+
+ boolean populateDepsInfo = (depsInfo != null)
+ Set<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+ List<MavenGrabRecord> grabRecords = []
+ for (Map dep : dependencies) {
+ MavenGrabRecord mgr = createGrabRecord(dep)
+ grabRecordsForCurrDependencies.add(mgr)
+ localDeps.add(mgr)
+ grabRecords.add(mgr)
+ }
+
+ try {
+ URI[] results = getDependencies(args, populateDepsInfo ? depsInfo
: null, grabRecords as MavenGrabRecord[])
+ return results
+ } catch (Exception e) {
+ localDeps.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+ throw e
+ }
+ }
+
+ URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord...
grabRecords) {
+ try (RepositorySystem system = new RepositorySystemSupplier().get()) {
+ def localRepo = new LocalRepository(getGrapeCacheDir())
+ String checksumPolicy = args.disableChecksums ?
+ RepositoryPolicy.CHECKSUM_POLICY_IGNORE :
+ RepositoryPolicy.CHECKSUM_POLICY_WARN // Use WARN instead of
FAIL for better compatibility
+
+ try (RepositorySystemSession.CloseableSession session = system
+ .createSessionBuilder()
+ .withLocalRepositories(localRepo)
+ .setChecksumPolicy(checksumPolicy)
+ .setSystemProperty('aether.artifactDescriptor.ignoreErrors',
'true')
+
.setConfigProperty('aether.artifactDescriptor.ignoreInvalidActivationExpressions',
'true')
+ .build()) {
+
+ List<URI> results = []
+
+ for (MavenGrabRecord grabRecord : grabRecords) {
+ String coords =
"${grabRecord.groupId()}:${grabRecord.module()}"
+ if (grabRecord.classifier()) {
+ coords += ":${grabRecord.classifier()}"
+ }
+ coords += ":${grabRecord.ext()}:${grabRecord.version()}"
+
+ Artifact artifact = new DefaultArtifact(coords)
+
+ String scope = grabRecord.conf()?.get(0) ?:
JavaScopes.COMPILE
+ if (scope == 'default') scope = JavaScopes.COMPILE
+
+ List<ArtifactResult> artifactResults
+ if (grabRecord.transitive()) {
+ DependencyFilter classpathFilter =
DependencyFilterUtils.classpathFilter(scope)
+ CollectRequest collectRequest = new CollectRequest(
+ root: new Dependency(artifact, scope),
+ repositories: repos
+ )
+ DependencyRequest dependencyRequest = new
DependencyRequest(collectRequest, classpathFilter)
+ artifactResults = system.resolveDependencies(session,
dependencyRequest).getArtifactResults()
+ } else {
+ ArtifactRequest artifactRequest = new
ArtifactRequest(artifact, repos, null)
+ artifactResults = [system.resolveArtifact(session,
artifactRequest)]
+ }
+
+ for (ArtifactResult found : artifactResults) {
+ if (found.artifact.file) {
+ results << found.artifact.file.toURI()
+
+ if (depsInfo != null) {
+ depsInfo << [
+ 'group': found.artifact.groupId,
+ 'module': found.artifact.artifactId,
+ 'version': found.artifact.version
+ ]
+ }
+ }
+ }
+ }
+
+ return results as URI[]
+ }
+ }
+ }
+
+ MavenGrabRecord createGrabRecord(Map dep) {
+ String module = dep.module ?: dep.artifactId ?: dep.artifact
+ if (!module) {
+ throw new RuntimeException('grab requires at least a module: or
artifactId: or artifact: argument')
+ }
+
+ // check for malformed components of the coordinates
+ dep.each { k, v ->
+ if (v instanceof CharSequence) {
+ if (k.toString().contains('v')) { // revision, version, rev
+ if (!(v ==~ '[^/:"<>|]*')) {
+ throw new RuntimeException("Grab: invalid value of
'$v' for $k: should not contain any of / \\ : \" < > |")
Review Comment:
The version validation regex `'[^/:"<>|]*'` is missing the backslash
character compared to the original `GrapeIvy` implementation which uses
`'[^\\\\/:"<>|]*'` (at `GrapeIvy.groovy:201`). The error message at line 311
mentions `\\` but the regex doesn't actually reject it. The regex should be
`'[^\\\\/:"<>|]*'` to match the GrapeIvy behavior.
##########
subprojects/groovy-grape-maven/src/main/groovy/groovy/grape/maven/GrapeMaven.groovy:
##########
@@ -0,0 +1,501 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.grape.maven
+
+import groovy.grape.GrapeEngine
+import groovy.grape.GrapeUtil
+import groovy.transform.AutoFinal
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.NamedParam
+import groovy.transform.NamedParams
+import org.codehaus.groovy.reflection.ReflectionUtils
+import org.eclipse.aether.RepositorySystem
+import org.eclipse.aether.RepositorySystemSession
+import org.eclipse.aether.artifact.Artifact
+import org.eclipse.aether.artifact.DefaultArtifact
+import org.eclipse.aether.collection.CollectRequest
+import org.eclipse.aether.graph.Dependency
+import org.eclipse.aether.graph.DependencyFilter
+import org.eclipse.aether.repository.LocalRepository
+import org.eclipse.aether.repository.RemoteRepository
+import org.eclipse.aether.repository.RepositoryPolicy
+import org.eclipse.aether.resolution.ArtifactRequest
+import org.eclipse.aether.resolution.ArtifactResult
+import org.eclipse.aether.resolution.DependencyRequest
+import org.eclipse.aether.supplier.RepositorySystemSupplier
+import org.eclipse.aether.util.artifact.JavaScopes
+import org.eclipse.aether.util.filter.DependencyFilterUtils
+
+/**
+ * Implementation supporting {@code @Grape} and {@code @Grab} annotations
based on Maven.
+ */
+@AutoFinal
+@CompileStatic
+class GrapeMaven implements GrapeEngine {
+ private static final List<String> DEFAULT_CONF =
Collections.singletonList('default')
+ private static final Map<String, Set<String>> MUTUALLY_EXCLUSIVE_KEYS =
processGrabArgs([
+ ['group', 'groupId', 'organisation', 'organization', 'org'],
+ ['module', 'artifactId', 'artifact'],
+ ['version', 'revision', 'rev'],
+ ['conf', 'scope', 'configuration'],
+ ])
+
+ @CompileDynamic // maps a->[b,c], b->[a,c] and c->[a,b] given [a,b,c]
+ private static Map<String, Set<String>> processGrabArgs(List<List<String>>
grabArgs) {
+ grabArgs.inject([:]) { Map m, List g -> g.each { a -> m[a] = (g - a)
as Set }; m }
+ }
+
+ // weak hash map so we don't leak loaders directly
+ final Map<ClassLoader, Set<MavenGrabRecord>> loadedDeps = [] as WeakHashMap
+ /** Stores the MavenGrabRecord(s) for all dependencies in each grab()
call. */
+ final Set<MavenGrabRecord> grabRecordsForCurrDependencies = [] as
LinkedHashSet
+ boolean enableGrapes = true
+ Set<String> downloadedArtifacts = []
+ Set<String> resolvedDependencies = []
+ final List<RemoteRepository> repos = [
+ new RemoteRepository.Builder("central", "default",
"https://repo.maven.apache.org/maven2/").build()
+ ]
+
+ @Override
+ grab(String endorsedModule) {
+ grab(group: 'groovy.endorsed', module: endorsedModule, version:
GroovySystem.getVersion())
+ }
+
+ @Override
+ grab(Map args) {
+ args.calleeDepth = args.calleeDepth ?: DEFAULT_CALLEE_DEPTH + 1
+ grab(args, args)
+ }
+
+ @Override
+ grab(Map args, Map... dependencies) {
+ ClassLoader loader = null
+ grabRecordsForCurrDependencies.clear()
+
+ try {
+ // identify the target classloader early, so we fail before
checking repositories
+ loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an
exception
+ if (!loader) return
+
+ URI[] uris = resolve(loader, args, dependencies)
+ for (URI uri : uris) {
+ GrapeUtil.addURL(loader, uri)
+ }
+ boolean runnerServicesFound = false
+ for (URI uri : uris) {
+ // TODO: check artifact type, jar vs library, etc.
+ File file = new File(uri)
+ GrapeUtil.processCategoryMethods(loader, file)
+ Collection<String> services =
GrapeUtil.processMetaInfServices(loader, file)
+ if (!runnerServicesFound) {
+ runnerServicesFound = GrapeUtil.checkForRunner(services)
+ }
+ }
+ if (runnerServicesFound) {
+ GrapeUtil.registryLoad(loader)
+ }
+ } catch (Exception e) {
+ // clean-up the state first
+ Set<MavenGrabRecord> grabRecordsForCurrLoader =
getLoadedDepsForLoader(loader)
+ grabRecordsForCurrLoader.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+
+ if (args.noExceptions) {
+ return e
+ }
+ throw e
+ }
+ null
+ }
+
+
+ @Override
+ @CompileDynamic
+ Map<String, Map<String, List<String>>> enumerateGrapes() {
+ Map<String, Map<String, List<String>>> bunches = [:]
+ File cacheRoot = grapeCacheDir.canonicalFile
+
+ cacheRoot.eachFileRecurse { File f ->
+ if (!f.isFile()) return
+ String name = f.name
+ if (name.endsWith('.sha1') || name.endsWith('.md5') ||
name.endsWith('.asc')) return
+
+ File versionDir = f.parentFile
+ File moduleDir = versionDir?.parentFile
+ File groupDir = moduleDir?.parentFile
+ if (!versionDir || !moduleDir || !groupDir) return
+
+ String version = versionDir.name
+ String module = moduleDir.name
+ String expectedPrefix = module + '-' + version
+ if (!name.startsWith(expectedPrefix)) return
+
+ String groupPath =
cacheRoot.toPath().relativize(groupDir.toPath()).toString()
+ if (!groupPath) return
+ String groupKey = groupPath.replace(File.separatorChar, '.' as
char)
+
+ Map<String, List<String>> modules =
bunches.computeIfAbsent(groupKey) { [:] }
+ List<String> versions = modules.computeIfAbsent(module) { [] }
+ if (!versions.contains(version)) {
+ versions << version
+ }
+ }
+
+ bunches.values()*.values()*.each { List<String> versions ->
versions.sort() }
+ bunches
+ }
+
+ void uninstallArtifact(String group, String module, String rev) {
+ String groupPath = group.replace('.' as char, File.separatorChar)
+ def artifactDir = new File(grapeCacheDir, groupPath + File.separator +
module + File.separator + rev)
+ if (artifactDir.exists()) {
+ artifactDir.deleteDir()
+ }
+ }
+
+ @Override
+ URI[] resolve(Map args, Map... dependencies) {
+ resolve(args, null, dependencies)
+ }
+
+ @Override
+ URI[] resolve(Map args, List depsInfo, Map... dependencies) {
+ // identify the target classloader early, so we fail before checking
repositories
+ ClassLoader loader = chooseClassLoader(
+ refObject: args.remove('refObject'),
+ classLoader: args.remove('classLoader'),
+ calleeDepth: args.calleeDepth ?: DEFAULT_CALLEE_DEPTH,
+ )
+
+ // check for non-fail null
+ // if we were in fail mode we would have already thrown an exception
+ if (!loader) {
+ return new URI[0]
+ }
+
+ resolve(loader, args, depsInfo, dependencies)
+ }
+
+ private Set<MavenGrabRecord> getLoadedDepsForLoader(ClassLoader loader) {
+ // use a LinkedHashSet to preserve the initial insertion order
+ loadedDeps.computeIfAbsent(loader, k -> [] as LinkedHashSet)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, Map... dependencies) {
+ resolve(loader, args, null, dependencies)
+ }
+
+ URI[] resolve(ClassLoader loader, Map args, List depsInfo, Map...
dependencies) {
+ if (!enableGrapes) {
+ return new URI[0]
+ }
+
+ boolean populateDepsInfo = (depsInfo != null)
+ Set<MavenGrabRecord> localDeps = getLoadedDepsForLoader(loader)
+ List<MavenGrabRecord> grabRecords = []
+ for (Map dep : dependencies) {
+ MavenGrabRecord mgr = createGrabRecord(dep)
+ grabRecordsForCurrDependencies.add(mgr)
+ localDeps.add(mgr)
+ grabRecords.add(mgr)
+ }
+
+ try {
+ URI[] results = getDependencies(args, populateDepsInfo ? depsInfo
: null, grabRecords as MavenGrabRecord[])
+ return results
+ } catch (Exception e) {
+ localDeps.removeAll(grabRecordsForCurrDependencies)
+ grabRecordsForCurrDependencies.clear()
+ throw e
+ }
+ }
+
+ URI[] getDependencies(Map args, List depsInfo, MavenGrabRecord...
grabRecords) {
+ try (RepositorySystem system = new RepositorySystemSupplier().get()) {
Review Comment:
A new `RepositorySystemSupplier().get()` (and hence a new
`RepositorySystem`) is created on every call to `getDependencies`. The
`RepositorySystemSupplier` bootstraps a significant amount of infrastructure
(service locator, transport providers, etc.). Consider creating the
`RepositorySystem` once (e.g., in the constructor or lazily) and reusing it
across calls, similar to how `GrapeIvy` creates its `Ivy` instance once in its
constructor.
> Support Maven Resolver based version of Grapes
> ----------------------------------------------
>
> Key: GROOVY-11871
> URL: https://issues.apache.org/jira/browse/GROOVY-11871
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Priority: Major
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)