This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4065a7464c45 CAMEL-25921: camel-tooling-maven - resolve artifacts
cached by Maven 3.10 and do not lose dependencies with preferLocal (#28239)
4065a7464c45 is described below
commit 4065a7464c45707ac0c85721c286242ef2a359f8
Author: Federico Mariani <[email protected]>
AuthorDate: Wed Oct 21 21:30:52 2026 +0200
CAMEL-25921: camel-tooling-maven - resolve artifacts cached by Maven 3.10
and do not lose dependencies with preferLocal (#28239)
Maven 3.10 (Maven Resolver 2.0.23+) tracks the origin of the artifacts in
_remote.repositories with URL-qualified keys (file>central-<sha1(url)>=),
which the embedded Maven Resolver 1.9 does not recognize. Accept them for
the requested repository id, as the ID-only keys, so the artifacts cached by
Maven 3.10 are neither verified again remotely nor unavailable offline.
preferLocal no longer resolves offline first, then online on failure: a POM
which could not be read offline silently resolved with no dependencies, and
the online retry reused it from the shared session cache. It now resolves
once, without checking the remote repositories for updates of what is
already cached (like mvn --no-snapshot-updates).
---
.../camel/main/download/DependencyDownloader.java | 5 +-
.../camel/tooling/maven/MavenDownloader.java | 8 +-
.../camel/tooling/maven/MavenDownloaderImpl.java | 77 +++------
.../TrackingKeyCompatLocalRepositoryManager.java | 160 +++++++++++++++++
.../tooling/maven/MavenDownloaderImplTest.java | 6 +-
.../maven/MavenDownloaderTrackingKeyTest.java | 189 +++++++++++++++++++++
6 files changed, 387 insertions(+), 58 deletions(-)
diff --git
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
index 755bfa39c2ed..81ef9f60ad57 100644
---
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
+++
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/DependencyDownloader.java
@@ -67,8 +67,9 @@ public interface DependencyDownloader extends
CamelContextAware, StaticService {
boolean isDownload();
/**
- * When enabled, prefer resolving from local Maven repository before
checking remote repositories. Falls back to
- * normal resolution if offline resolution fails. Useful for CLI tools
where artifacts are typically already cached.
+ * When enabled, prefer the artifacts already in the local Maven
repository, without checking the remote
+ * repositories for updates; missing artifacts are still downloaded.
Useful for CLI tools where artifacts are
+ * typically already cached.
*/
void setPreferLocal(boolean preferLocal);
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
index 46947617e2b9..0a06b732d336 100644
---
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
@@ -105,10 +105,10 @@ public interface MavenDownloader extends Service {
void setOffline(boolean offline);
/**
- * When enabled, resolution will first check if artifacts exist in the
local Maven repository and attempt offline
- * resolution to avoid remote repository checks. Falls back to normal
resolution if offline resolution fails. This
- * is useful for CLI tools where artifacts are typically already cached
and remote SNAPSHOT metadata checks add
- * significant latency.
+ * When enabled, the artifacts and metadata already in the local Maven
repository are used as is, without checking
+ * the remote repositories for updates (like {@code mvn
--no-snapshot-updates}), while missing artifacts are still
+ * downloaded. This is useful for CLI tools where artifacts are typically
already cached and remote SNAPSHOT
+ * metadata checks add significant latency.
*/
void setPreferLocal(boolean preferLocal);
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
index 5f731482c9a1..27e9e5e1b4bb 100644
---
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
@@ -18,13 +18,11 @@ package org.apache.camel.tooling.maven;
import java.io.BufferedInputStream;
import java.io.File;
-import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -116,6 +114,7 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
// Maven Resolver components
private RepositorySystem repositorySystem;
private RepositorySystemSession repositorySystemSession;
+ private RepositorySystemSession preferLocalSession;
private Settings settings;
// Embedded mode flag
@@ -340,7 +339,7 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
// Get RepositorySystem and RepositorySystemSession from MIMA
repositorySystem = mimaContext.repositorySystem();
- repositorySystemSession = mimaContext.repositorySystemSession();
+ repositorySystemSession =
withTrackingKeyCompat(mimaContext.repositorySystemSession());
// Configure repositories (same as embedded mode)
List<RemoteRepository> originalRepositories = new ArrayList<>();
@@ -567,25 +566,20 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
boolean transitively, boolean useApacheSnapshots)
throws MavenResolutionException {
- // When preferLocal is enabled and not already offline, try offline
resolution first
- // if the requested artifacts exist in the local Maven repository.
This avoids expensive
- // remote SNAPSHOT metadata checks for artifacts that are already
cached locally.
- if (preferLocal && !offline && existsInLocalRepo(dependencyGAVs)) {
- try {
- DefaultRepositorySystemSession offlineSession
- = new
DefaultRepositorySystemSession(repositorySystemSession);
- offlineSession.setOffline(true);
- return doResolveArtifacts(rootGav, dependencyGAVs,
extraRepositories,
- transitively, useApacheSnapshots, offlineSession);
- } catch (MavenResolutionException e) {
- LOG.debug("Offline resolution failed for locally cached
artifacts, "
- + "falling back to online: {}",
- e.getMessage());
- }
- }
-
+ // When preferLocal is enabled, what is already in the local
repository is used as is: SNAPSHOT
+ // metadata is not checked again remotely, while anything missing is
still downloaded
+ RepositorySystemSession session = preferLocal && !fresh ?
getPreferLocalSession() : repositorySystemSession;
return doResolveArtifacts(rootGav, dependencyGAVs, extraRepositories,
- transitively, useApacheSnapshots, repositorySystemSession);
+ transitively, useApacheSnapshots, session);
+ }
+
+ private RepositorySystemSession getPreferLocalSession() {
+ if (preferLocalSession == null) {
+ DefaultRepositorySystemSession session = new
DefaultRepositorySystemSession(repositorySystemSession);
+ session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_NEVER);
+ preferLocalSession = session;
+ }
+ return preferLocalSession;
}
private List<MavenArtifact> doResolveArtifacts(
@@ -692,33 +686,6 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
}
}
- /**
- * Checks if all the given dependency artifacts exist in the local Maven
repository. For each artifact, verifies
- * that the version directory exists and contains at least one JAR file.
This handles both locally-installed
- * SNAPSHOTs (artifact-version.jar) and remotely-downloaded SNAPSHOTs
(artifact-timestamp-buildnumber.jar).
- */
- private boolean existsInLocalRepo(List<String> dependencyGAVs) {
- File basedir =
repositorySystemSession.getLocalRepository().getBasedir();
- for (String gav : dependencyGAVs) {
- MavenGav mg = MavenGav.parseGav(gav);
- Path versionDir = basedir.toPath()
- .resolve(mg.getGroupId().replace('.', File.separatorChar))
- .resolve(mg.getArtifactId())
- .resolve(mg.getVersion());
- if (!Files.isDirectory(versionDir)) {
- return false;
- }
- try (var stream = Files.list(versionDir)) {
- if (stream.noneMatch(p ->
p.getFileName().toString().endsWith(".jar"))) {
- return false;
- }
- } catch (IOException e) {
- return false;
- }
- }
- return true;
- }
-
@Override
public List<MavenGav> resolveAvailableVersions(String groupId, String
artifactId, String repository)
throws MavenResolutionException {
@@ -861,7 +828,8 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
try {
LocalRepositoryManager lrm =
repositorySystem.newLocalRepositoryManager(rssCopy,
new LocalRepository(localRepository));
- rssCopy.setLocalRepositoryManager(lrm);
+ rssCopy.setLocalRepositoryManager(embeddedMode
+ ? lrm :
TrackingKeyCompatLocalRepositoryManager.wrap(rssCopy, lrm));
} catch (Exception e) {
LOG.warn("Failed to set custom local repository: {}",
e.getMessage(), e);
}
@@ -883,6 +851,17 @@ public class MavenDownloaderImpl extends ServiceSupport
implements MavenDownload
this.remoteArtifactDownloadListener = remoteArtifactDownloadListener;
}
+ /**
+ * Lets the local repository manager accept the artifacts tracked by Maven
3.10+ as downloaded from the requested
+ * repositories. See {@link TrackingKeyCompatLocalRepositoryManager}.
+ */
+ private static RepositorySystemSession
withTrackingKeyCompat(RepositorySystemSession session) {
+ DefaultRepositorySystemSession compat = new
DefaultRepositorySystemSession(session);
+ compat.setLocalRepositoryManager(
+ TrackingKeyCompatLocalRepositoryManager.wrap(compat,
session.getLocalRepositoryManager()));
+ return compat;
+ }
+
private static MavenArtifact toMavenArtifact(ArtifactResult ar) {
Artifact artifact = ar.getArtifact();
MavenGav gav = MavenGav.fromCoordinates(
diff --git
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/TrackingKeyCompatLocalRepositoryManager.java
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/TrackingKeyCompatLocalRepositoryManager.java
new file mode 100644
index 000000000000..0ced0c2c14c7
--- /dev/null
+++
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/TrackingKeyCompatLocalRepositoryManager.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.apache.camel.tooling.maven;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.metadata.Metadata;
+import org.eclipse.aether.repository.LocalArtifactRegistration;
+import org.eclipse.aether.repository.LocalArtifactRequest;
+import org.eclipse.aether.repository.LocalArtifactResult;
+import org.eclipse.aether.repository.LocalMetadataRegistration;
+import org.eclipse.aether.repository.LocalMetadataRequest;
+import org.eclipse.aether.repository.LocalMetadataResult;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.util.ConfigUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link LocalRepositoryManager} that also accepts the artifacts a newer
Maven Resolver tracked as downloaded from
+ * the requested repository.
+ * <p>
+ * Maven 3.10 (Maven Resolver 2.0.23+) records the origin of a downloaded
artifact in {@code _remote.repositories} with
+ * a URL-qualified key ({@code file>central-<sha1(url)>=}), while Maven
Resolver 1.9 only knows the ID-only key
+ * ({@code file>central=}). Without this, every artifact downloaded by Maven
3.10 is considered as not downloaded from
+ * the requested repository: it cannot be resolved offline and it is verified
again against the remote repositories.
+ * <p>
+ * An URL-qualified entry is accepted for a repository with the same id,
whatever the URL, which is the trust level of
+ * the ID-only entries (and what Maven Resolver 2.0.23 itself does when it
reads ID-only entries). What is written to
+ * the local repository is left untouched, so it stays readable by both old
and new Maven versions.
+ */
+class TrackingKeyCompatLocalRepositoryManager implements
LocalRepositoryManager {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(TrackingKeyCompatLocalRepositoryManager.class);
+
+ private static final String CONFIG_PROP_TRACKING_FILENAME =
"aether.enhancedLocalRepository.trackingFilename";
+ private static final String DEFAULT_TRACKING_FILENAME =
"_remote.repositories";
+ // the SHA-1 of the URL (nid_hurl), optionally followed by the one of the
context for repository managers
+ private static final String URL_QUALIFIER =
"-[0-9a-f]{40}(-[0-9a-f]{40})?";
+
+ private final LocalRepositoryManager delegate;
+ private final String trackingFilename;
+
+ TrackingKeyCompatLocalRepositoryManager(RepositorySystemSession session,
LocalRepositoryManager delegate) {
+ this.delegate = delegate;
+ this.trackingFilename
+ = ConfigUtils.getString(session, DEFAULT_TRACKING_FILENAME,
CONFIG_PROP_TRACKING_FILENAME);
+ }
+
+ /**
+ * Wraps the local repository manager of the given session, unless it is
already wrapped.
+ */
+ static LocalRepositoryManager wrap(RepositorySystemSession session,
LocalRepositoryManager lrm) {
+ if (lrm == null || lrm instanceof
TrackingKeyCompatLocalRepositoryManager) {
+ return lrm;
+ }
+ return new TrackingKeyCompatLocalRepositoryManager(session, lrm);
+ }
+
+ @Override
+ public LocalArtifactResult find(RepositorySystemSession session,
LocalArtifactRequest request) {
+ LocalArtifactResult result = delegate.find(session, request);
+ if (!result.isAvailable() && result.getFile() != null &&
result.getFile().isFile()) {
+ File file = result.getFile();
+ Properties props = readTrackingFile(file);
+ for (RemoteRepository repository : request.getRepositories()) {
+ Pattern key = Pattern.compile(Pattern.quote(file.getName() +
'>' + repository.getId()) + URL_QUALIFIER);
+ if (props.keySet().stream().anyMatch(k ->
key.matcher(k.toString()).matches())) {
+ LOG.debug("Accepting {} tracked with an URL-qualified key
of repository {}", file,
+ repository.getId());
+ result.setAvailable(true);
+ result.setRepository(repository);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private Properties readTrackingFile(File artifactFile) {
+ Properties props = new Properties();
+ File trackingFile = new File(artifactFile.getParentFile(),
trackingFilename);
+ if (trackingFile.isFile()) {
+ try (InputStream is = Files.newInputStream(trackingFile.toPath()))
{
+ props.load(is);
+ } catch (IOException e) {
+ LOG.debug("Cannot read {}: {}", trackingFile, e.getMessage(),
e);
+ }
+ }
+ return props;
+ }
+
+ @Override
+ public LocalRepository getRepository() {
+ return delegate.getRepository();
+ }
+
+ @Override
+ public String getPathForLocalArtifact(Artifact artifact) {
+ return delegate.getPathForLocalArtifact(artifact);
+ }
+
+ @Override
+ public String getPathForRemoteArtifact(Artifact artifact, RemoteRepository
repository, String context) {
+ return delegate.getPathForRemoteArtifact(artifact, repository,
context);
+ }
+
+ @Override
+ public String getPathForLocalMetadata(Metadata metadata) {
+ return delegate.getPathForLocalMetadata(metadata);
+ }
+
+ @Override
+ public String getPathForRemoteMetadata(Metadata metadata, RemoteRepository
repository, String context) {
+ return delegate.getPathForRemoteMetadata(metadata, repository,
context);
+ }
+
+ @Override
+ public void add(RepositorySystemSession session, LocalArtifactRegistration
request) {
+ delegate.add(session, request);
+ }
+
+ @Override
+ public LocalMetadataResult find(RepositorySystemSession session,
LocalMetadataRequest request) {
+ return delegate.find(session, request);
+ }
+
+ @Override
+ public void add(RepositorySystemSession session, LocalMetadataRegistration
request) {
+ delegate.add(session, request);
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+}
diff --git
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
index a21caf322e4d..d4f1106df3b1 100644
---
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
+++
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
@@ -311,7 +311,7 @@ class MavenDownloaderImplTest {
// Now create a new downloader with preferLocal=true, Maven Central
disabled,
// and a non-existent fake repo. Without preferLocal, this would fail
because
// there are no reachable remote repos. With preferLocal, it should
succeed
- // by resolving from the local cache via offline-first resolution.
+ // by using the local cache without checking the remote repositories.
try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
downloader.setPreferLocal(true);
downloader.setMavenCentralEnabled(false);
@@ -335,9 +335,9 @@ class MavenDownloaderImplTest {
@Test
@EnabledIfSystemProperty(named = "enableMavenDownloaderTests", matches =
"true")
- void testPreferLocalFallsBackToOnline() throws Exception {
+ void testPreferLocalDownloadsMissingArtifact() throws Exception {
// With preferLocal=true, if the artifact is NOT in the local repo,
- // it should fall back to online resolution and succeed
+ // it is still downloaded
try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
downloader.setPreferLocal(true);
downloader.build();
diff --git
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderTrackingKeyTest.java
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderTrackingKeyTest.java
new file mode 100644
index 000000000000..88e4e211de86
--- /dev/null
+++
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderTrackingKeyTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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 org.apache.camel.tooling.maven;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Local repositories written by Maven 3.10 (Maven Resolver 2.0.23+) track the
origin of artifacts with URL-qualified
+ * keys ({@code file>central-<sha1(url)>=}) instead of the ID-only keys
({@code file>central=}) of older Maven versions.
+ * Both forms must be accepted, for the same repository id.
+ */
+public class MavenDownloaderTrackingKeyTest {
+
+ private static final String REPO_ID = "myrepo";
+ // Maven hashes the URL it knows the repository by, which may differ from
the URL Camel uses (e.g. Maven Central)
+ private static final String OTHER_URL_KEY = REPO_ID + "-" +
DigestUtils.sha1Hex("https://repo.example.com/maven2");
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void testResolveArtifactTrackedWithLegacyKey() throws Exception {
+ Path localRepo = tempDir.resolve("local");
+ writeArtifact(localRepo, "dep", "", null, REPO_ID);
+
+ // the remote repository is unreachable, so the artifact can only come
from the local repository
+ assertEquals(List.of("dep-1.0.jar"), resolve(localRepo,
"http://localhost:1/unreachable", "dep", false));
+ }
+
+ @Test
+ void testResolveArtifactTrackedWithUrlQualifiedKey() throws Exception {
+ Path localRepo = tempDir.resolve("local");
+ writeArtifact(localRepo, "dep", "", null, OTHER_URL_KEY);
+
+ assertEquals(List.of("dep-1.0.jar"), resolve(localRepo,
"http://localhost:1/unreachable", "dep", false));
+ }
+
+ @Test
+ void testUrlQualifiedKeyOfAnotherRepositoryIsNotAccepted() throws
Exception {
+ Path localRepo = tempDir.resolve("local");
+ // "myrepo-other" is a different repository whose key also starts with
"myrepo-"
+ writeArtifact(localRepo, "dep", "", null,
+ REPO_ID + "-other-" +
DigestUtils.sha1Hex("https://other.example.com"));
+
+ // not trusted, so it has to be verified against the (unreachable)
remote repository
+ assertThrows(MavenResolutionException.class,
+ () -> resolve(localRepo, "http://localhost:1/unreachable",
"dep", false));
+ }
+
+ @Test
+ void testResolveTransitivelyWithBomTrackedWithUrlQualifiedKey() throws
Exception {
+ Path localRepo = tempDir.resolve("local");
+ Path remoteRepo = tempDir.resolve("remote");
+ writePluginProject(localRepo, OTHER_URL_KEY);
+ writePluginProject(remoteRepo, null);
+
+ assertEquals(List.of("plugin-1.0.jar", "dep-1.0.jar"),
+ resolve(localRepo, remoteRepo.toUri().toString(), "plugin",
true));
+ }
+
+ @Test
+ void testResolveTransitivelyWithBomMissingInLocalRepository() throws
Exception {
+ Path localRepo = tempDir.resolve("local");
+ Path remoteRepo = tempDir.resolve("remote");
+ writePluginProject(localRepo, REPO_ID);
+ writePluginProject(remoteRepo, null);
+ // the plugin POM cannot be built from the local repository alone, so
the BOM has to be downloaded
+ deleteDirectory(localRepo.resolve("org/test/bom"));
+
+ assertEquals(List.of("plugin-1.0.jar", "dep-1.0.jar"),
+ resolve(localRepo, remoteRepo.toUri().toString(), "plugin",
true));
+ }
+
+ /**
+ * A plugin whose parent imports a BOM that manages the version of its
dependency.
+ *
+ * @param key the tracking key of the downloaded artifacts, {@code null}
for a remote repository; the plugin and its
+ * parent are installed locally (mvn install)
+ */
+ private static void writePluginProject(Path repo, String key) throws
IOException {
+ boolean local = key != null;
+ writePom(repo, "parent", "<packaging>pom</packaging>"
+ +
"<dependencyManagement><dependencies><dependency>"
+ +
"<groupId>org.test</groupId><artifactId>bom</artifactId><version>1.0</version>"
+ + "<type>pom</type><scope>import</scope>"
+ +
"</dependency></dependencies></dependencyManagement>",
+ local ? "" : null);
+ writePom(repo, "bom", "<packaging>pom</packaging>"
+ +
"<dependencyManagement><dependencies><dependency>"
+ +
"<groupId>org.test</groupId><artifactId>dep</artifactId><version>1.0</version>"
+ +
"</dependency></dependencies></dependencyManagement>",
+ key);
+ writeArtifact(repo, "plugin",
+
"<parent><groupId>org.test</groupId><artifactId>parent</artifactId><version>1.0</version></parent>"
+ + "<dependencies><dependency>"
+ +
"<groupId>org.test</groupId><artifactId>dep</artifactId>"
+ + "</dependency></dependencies>",
+ local ? "" : null, null);
+ writeArtifact(repo, "dep", "", key, null);
+ }
+
+ private static void deleteDirectory(Path dir) throws IOException {
+ try (Stream<Path> paths = Files.walk(dir)) {
+ for (Path p : paths.sorted(Comparator.reverseOrder()).toList()) {
+ Files.delete(p);
+ }
+ }
+ }
+
+ private List<String> resolve(Path localRepo, String remoteUrl, String
artifactId, boolean transitively)
+ throws Exception {
+ try (MavenDownloaderImpl downloader = newDownloader(remoteUrl)) {
+ return downloader.customize(localRepo.toString(), 1000, 1000)
+ .resolveArtifacts(List.of("org.test:" + artifactId +
":1.0"), null, transitively, false)
+ .stream().map(a ->
a.getFile().getName()).collect(Collectors.toList());
+ }
+ }
+
+ private static MavenDownloaderImpl newDownloader(String remoteUrl) {
+ MavenDownloaderImpl downloader = new MavenDownloaderImpl();
+ downloader.setMavenCentralEnabled(false);
+ downloader.setMavenApacheSnapshotEnabled(false);
+ downloader.setMavenSettingsLocation("false");
+ downloader.setRepos(REPO_ID + "=" + remoteUrl);
+ downloader.build();
+ return downloader;
+ }
+
+ /**
+ * @param pomKey the tracking key of the POM, {@code ""} if installed
locally, {@code null} for no tracking file
+ * @param jarKey the tracking key of the JAR, defaults to the POM's
+ */
+ private static void writeArtifact(Path repo, String artifactId, String
pomBody, String pomKey, String jarKey)
+ throws IOException {
+ writePom(repo, artifactId, pomBody, pomKey);
+ Path dir = versionDir(repo, artifactId);
+ Files.writeString(dir.resolve(artifactId + "-1.0.jar"), artifactId);
+ String key = jarKey != null ? jarKey : pomKey;
+ if (key != null) {
+ Files.writeString(dir.resolve("_remote.repositories"),
+ artifactId + "-1.0.jar>" + key + "=\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ }
+ }
+
+ private static void writePom(Path repo, String artifactId, String body,
String key) throws IOException {
+ Path dir = versionDir(repo, artifactId);
+ Files.createDirectories(dir);
+ Files.writeString(dir.resolve(artifactId + "-1.0.pom"),
+ "<project><modelVersion>4.0.0</modelVersion>"
+ +
"<groupId>org.test</groupId><artifactId>" + artifactId
+ +
"</artifactId><version>1.0</version>" + body
+ +
"</project>");
+ if (key != null) {
+ Files.writeString(dir.resolve("_remote.repositories"), artifactId
+ "-1.0.pom>" + key + "=\n");
+ }
+ }
+
+ private static Path versionDir(Path repo, String artifactId) {
+ return
repo.resolve("org").resolve("test").resolve(artifactId).resolve("1.0");
+ }
+}