From 3a1deaa2710092be608588106a644f812e9b320c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 17:22:57 +0200 Subject: [PATCH 1/2] Remove the maven-compat dependency GetMojo used the legacy org.apache.maven.repository.RepositorySystem, whose only implementation, LegacyRepositorySystem, lives in maven-compat. It used it for nothing but injectMirror/injectProxy/injectAuthentication on the repositories named by the remoteRepositories parameter. org.apache.maven.bridge.MavenRepositorySystem is the usual replacement but a plugin cannot use it: maven-core does not export org.apache.maven.bridge to plugin class realms, so referencing it compiles, passes every unit test, and then fails at runtime with NoClassDefFoundError. Do the same selection in the plugin instead, in RepositorySessionInjector, against the repository session's mirror, proxy and authentication selectors -- Maven builds those from the same settings, already decrypted, and Resolver consults them itself when it transfers. Everything it touches is in a package maven-core does export. Also copy SnapshotArtifactRepositoryMetadata from maven-compat into the test tree, the only other thing the plugin needed from it, and cover the proxy and credential paths, which had no test that failed when the injection was removed. --- pom.xml | 8 - .../maven/plugins/dependency/GetMojo.java | 18 +- .../utils/RepositorySessionInjector.java | 193 ++++++++++++++++++ .../SnapshotArtifactRepositoryMetadata.java | 97 +++++++++ .../maven/plugins/dependency/TestGetMojo.java | 184 ++++++++++++++++- .../utils/RepositorySessionInjectorTest.java | 184 +++++++++++++++++ 6 files changed, 659 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java create mode 100644 src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java create mode 100644 src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java diff --git a/pom.xml b/pom.xml index af6f70762..900d746f6 100644 --- a/pom.xml +++ b/pom.xml @@ -370,14 +370,6 @@ under the License. test - - - org.apache.maven - maven-compat - ${mavenVersion} - test - - org.eclipse.jetty jetty-server diff --git a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java index 48d37908d..fafb54fe4 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java @@ -38,10 +38,9 @@ import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.dependency.utils.RepositorySessionInjector; import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.ProjectBuildingRequest; -import org.apache.maven.repository.RepositorySystem; -import org.apache.maven.settings.Settings; import org.apache.maven.shared.transfer.artifact.ArtifactCoordinate; import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate; import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver; @@ -73,9 +72,10 @@ public class GetMojo extends AbstractMojo { private final Map repositoryLayouts; /** - * The repository system. + * Applies the mirrors, proxies and credentials of the repository session to the repositories named by + * {@link #remoteRepositories}. */ - private final RepositorySystem repositorySystem; + private final RepositorySessionInjector repositorySessionInjector; private final DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate(); @@ -115,14 +115,13 @@ public GetMojo( ArtifactResolver artifactResolver, DependencyResolver dependencyResolver, ArtifactHandlerManager artifactHandlerManager, - Map repositoryLayouts, - RepositorySystem repositorySystem) { + Map repositoryLayouts) { this.session = session; this.artifactResolver = artifactResolver; this.dependencyResolver = dependencyResolver; this.artifactHandlerManager = artifactHandlerManager; this.repositoryLayouts = repositoryLayouts; - this.repositorySystem = repositorySystem; + this.repositorySessionInjector = new RepositorySessionInjector(repositoryLayouts); } @Override @@ -174,10 +173,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest()); - Settings settings = session.getSettings(); - repositorySystem.injectMirror(repoList, settings.getMirrors()); - repositorySystem.injectProxy(repoList, settings.getProxies()); - repositorySystem.injectAuthentication(repoList, settings.getServers()); + repositorySessionInjector.inject(session.getRepositorySession(), repoList); buildingRequest.setRemoteRepositories(repoList); diff --git a/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java b/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java new file mode 100644 index 000000000..e11a947f8 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java @@ -0,0 +1,193 @@ +/* + * 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.maven.plugins.dependency.utils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; +import org.apache.maven.artifact.repository.Authentication; +import org.apache.maven.artifact.repository.MavenArtifactRepository; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout2; +import org.apache.maven.repository.Proxy; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.AuthenticationSelector; +import org.eclipse.aether.repository.MirrorSelector; +import org.eclipse.aether.repository.ProxySelector; +import org.eclipse.aether.repository.RemoteRepository; + +/** + * Applies the mirror, proxy and authentication configuration of a {@link RepositorySystemSession} to repositories + * that were built by hand rather than obtained from the project, so that they can be used for resolution. + *

+ * This is the work {@code org.apache.maven.bridge.MavenRepositorySystem} does in its session-based + * {@code injectMirror}/{@code injectProxy}/{@code injectAuthentication} methods, reimplemented here because + * {@code org.apache.maven.bridge} is not one of the packages maven-core exports to plugin class realms — a plugin + * that references it compiles and unit-tests cleanly and then fails at runtime with + * {@code NoClassDefFoundError: org/apache/maven/bridge/MavenRepositorySystem}. Everything used below is in a package + * maven-core does export: {@code org.apache.maven} (for {@link RepositoryUtils}), {@code org.apache.maven.artifact}, + * {@code org.apache.maven.repository} and {@code org.eclipse.aether.repository}. + *

+ * The session's selectors are populated by Maven from the mirrors, proxies and servers of + * settings.xml, with the servers and proxies already decrypted, and they are the same selectors + * Resolver consults when it performs the transfer. + */ +public class RepositorySessionInjector { + + private final Map repositoryLayouts; + + public RepositorySessionInjector(Map repositoryLayouts) { + this.repositoryLayouts = repositoryLayouts; + } + + /** + * Applies the session's mirror, then proxy, then authentication configuration to each repository, in that order: + * mirroring rewrites the id and URL a repository is known by, and the proxy and the credentials are selected for + * the mirror rather than for the repository it replaced. + * + * @param session the repository session, may be {@code null} + * @param repositories the repositories to modify in place, may be {@code null} + */ + public void inject(RepositorySystemSession session, List repositories) { + if (session == null || repositories == null) { + return; + } + + for (ArtifactRepository repository : repositories) { + injectMirror(session, repository); + repository.setProxy(getProxy(session, repository)); + repository.setAuthentication(getAuthentication(session, repository)); + } + } + + private void injectMirror(RepositorySystemSession session, ArtifactRepository repository) { + MirrorSelector selector = session.getMirrorSelector(); + if (selector == null) { + return; + } + + RemoteRepository mirror = selector.getMirror(RepositoryUtils.toRepo(repository)); + if (mirror == null) { + return; + } + + repository.setMirroredRepositories(Collections.singletonList(createArtifactRepository( + repository.getId(), + repository.getUrl(), + repository.getLayout(), + repository.getSnapshots(), + repository.getReleases()))); + + repository.setId(mirror.getId()); + repository.setUrl(mirror.getUrl()); + + String layoutId = mirror.getContentType(); + if (layoutId != null && !layoutId.isEmpty()) { + repository.setLayout(getLayout(layoutId)); + } + + repository.setBlocked(mirror.isBlocked()); + } + + /** + * Same as {@code MavenRepositorySystem.createArtifactRepository}, including its substitution of default policies + * for null ones — {@link MavenArtifactRepository} stores null and fails later at whichever call site validates. + */ + private static ArtifactRepository createArtifactRepository( + String id, + String url, + ArtifactRepositoryLayout layout, + ArtifactRepositoryPolicy snapshots, + ArtifactRepositoryPolicy releases) { + ArtifactRepositoryPolicy snapshotPolicy = snapshots != null ? snapshots : new ArtifactRepositoryPolicy(); + ArtifactRepositoryPolicy releasePolicy = releases != null ? releases : new ArtifactRepositoryPolicy(); + + if (layout instanceof ArtifactRepositoryLayout2) { + return ((ArtifactRepositoryLayout2) layout) + .newMavenArtifactRepository(id, url, snapshotPolicy, releasePolicy); + } + return new MavenArtifactRepository(id, url, layout, snapshotPolicy, releasePolicy); + } + + /** + * Resolves a mirror's layout id. Maven core wraps an unknown id in a layout that delegates to the default one; + * falling back to the default layout outright computes the same paths. + */ + private ArtifactRepositoryLayout getLayout(String layoutId) { + ArtifactRepositoryLayout layout = repositoryLayouts.get(layoutId); + return layout != null ? layout : repositoryLayouts.get("default"); + } + + private Proxy getProxy(RepositorySystemSession session, ArtifactRepository repository) { + ProxySelector selector = session.getProxySelector(); + if (selector == null) { + return null; + } + + RemoteRepository repo = RepositoryUtils.toRepo(repository); + org.eclipse.aether.repository.Proxy proxy = selector.getProxy(repo); + if (proxy == null) { + return null; + } + + Proxy result = new Proxy(); + result.setHost(proxy.getHost()); + result.setProtocol(proxy.getType()); + result.setPort(proxy.getPort()); + + if (proxy.getAuthentication() != null) { + repo = new RemoteRepository.Builder(repo).setProxy(proxy).build(); + try (AuthenticationContext authCtx = AuthenticationContext.forProxy(session, repo)) { + result.setUserName(authCtx.get(AuthenticationContext.USERNAME)); + result.setPassword(authCtx.get(AuthenticationContext.PASSWORD)); + result.setNtlmDomain(authCtx.get(AuthenticationContext.NTLM_DOMAIN)); + result.setNtlmHost(authCtx.get(AuthenticationContext.NTLM_WORKSTATION)); + } + } + + return result; + } + + private Authentication getAuthentication(RepositorySystemSession session, ArtifactRepository repository) { + AuthenticationSelector selector = session.getAuthenticationSelector(); + if (selector == null) { + return null; + } + + RemoteRepository repo = RepositoryUtils.toRepo(repository); + org.eclipse.aether.repository.Authentication auth = selector.getAuthentication(repo); + if (auth == null) { + return null; + } + + repo = new RemoteRepository.Builder(repo).setAuthentication(auth).build(); + try (AuthenticationContext authCtx = AuthenticationContext.forRepository(session, repo)) { + Authentication result = new Authentication( + authCtx.get(AuthenticationContext.USERNAME), authCtx.get(AuthenticationContext.PASSWORD)); + result.setPrivateKey(authCtx.get(AuthenticationContext.PRIVATE_KEY_PATH)); + result.setPassphrase(authCtx.get(AuthenticationContext.PRIVATE_KEY_PASSPHRASE)); + return result; + } + } +} diff --git a/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java b/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java new file mode 100644 index 000000000..13d3eb6ec --- /dev/null +++ b/src/test/java/org/apache/maven/artifact/repository/metadata/SnapshotArtifactRepositoryMetadata.java @@ -0,0 +1,97 @@ +/* + * 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.maven.artifact.repository.metadata; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; + +/** + * Metadata for the artifact version directory of the repository. + *

+ * Class copied verbatim from maven-compat so that the maven-compat dependency can be dropped. It is used by the + * unit tests only, never by the plugin itself, and it is a plain data holder over + * {@link AbstractRepositoryMetadata} (which lives in maven-core), so copying it is enough — there is no component + * implementation left behind in maven-compat. Same technique as maven-plugin-plugin used for + * {@code GroupRepositoryMetadata} in MPLUGIN-384. + * + * @author Brett Porter + */ +public class SnapshotArtifactRepositoryMetadata extends AbstractRepositoryMetadata { + private final Artifact artifact; + + public SnapshotArtifactRepositoryMetadata(Artifact artifact) { + super(createMetadata(artifact, null)); + this.artifact = artifact; + } + + public SnapshotArtifactRepositoryMetadata(Artifact artifact, Snapshot snapshot) { + super(createMetadata(artifact, createVersioning(snapshot))); + this.artifact = artifact; + } + + @Override + public boolean storedInGroupDirectory() { + return false; + } + + @Override + public boolean storedInArtifactVersionDirectory() { + return true; + } + + @Override + public String getGroupId() { + return artifact.getGroupId(); + } + + @Override + public String getArtifactId() { + return artifact.getArtifactId(); + } + + @Override + public String getBaseVersion() { + return artifact.getBaseVersion(); + } + + @Override + public Object getKey() { + return "snapshot " + artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getBaseVersion(); + } + + @Override + public boolean isSnapshot() { + return artifact.isSnapshot(); + } + + @Override + public int getNature() { + return isSnapshot() ? SNAPSHOT : RELEASE; + } + + @Override + public ArtifactRepository getRepository() { + return artifact.getRepository(); + } + + @Override + public void setRepository(ArtifactRepository remoteRepository) { + artifact.setRepository(remoteRepository); + } +} diff --git a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java index 2cdb47831..028500278 100644 --- a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java +++ b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java @@ -21,6 +21,8 @@ import javax.inject.Inject; import java.net.InetAddress; +import java.net.URI; +import java.nio.file.Path; import java.util.Collections; import org.apache.maven.api.plugin.testing.Basedir; @@ -31,10 +33,18 @@ import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.DefaultProjectBuildingRequest; +import org.apache.maven.settings.Proxy; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.HashLoginService; @@ -46,10 +56,13 @@ import org.eclipse.jetty.util.security.Constraint; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.apache.maven.api.plugin.testing.MojoExtension.getTestPath; import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @@ -57,9 +70,17 @@ @Basedir("/unit/get-test") class TestGetMojo { + private static final String PROXY_HOST = "proxy.invalid"; + @Inject private MavenSession session; + @Inject + private RepositorySystem repositorySystem; + + @TempDir + private Path isolatedLocalRepository; + @BeforeEach void setUp() { Settings settings = new Settings(); @@ -72,6 +93,57 @@ void setUp() { settings.addServer(server); } + /** + * Mirrors the {@code } and {@code } entries of the session settings into the repository session, + * the way {@code DefaultRepositorySystemSessionFactory} does when Maven builds that session for real. GetMojo + * reads credentials and proxies from the session's selectors rather than from the {@link Settings} object, since + * those selectors are what Resolver itself consults when it performs the transfer, and the test harness has + * already created the repository session by the time {@link #setUp()} declares any of this. + *

+ * The local repository is swapped for an empty one at the same time, so that the tests below depend on the + * transfer actually happening rather than on what an earlier test left in the shared local repository. + */ + private void applySettingsToRepositorySession() { + Settings settings = session.getSettings(); + + DefaultAuthenticationSelector authenticationSelector = new DefaultAuthenticationSelector(); + for (Server server : settings.getServers()) { + authenticationSelector.add( + server.getId(), + new AuthenticationBuilder() + .addUsername(server.getUsername()) + .addPassword(server.getPassword()) + .addPrivateKey(server.getPrivateKey(), server.getPassphrase()) + .build()); + } + + DefaultProxySelector proxySelector = new DefaultProxySelector(); + for (Proxy proxy : settings.getProxies()) { + if (!proxy.isActive()) { + // Maven filters the inactive ones out before they ever reach the selector + continue; + } + proxySelector.add( + new org.eclipse.aether.repository.Proxy( + proxy.getProtocol(), + proxy.getHost(), + proxy.getPort(), + new AuthenticationBuilder() + .addUsername(proxy.getUsername()) + .addPassword(proxy.getPassword()) + .build()), + proxy.getNonProxyHosts()); + } + + DefaultRepositorySystemSession repositorySession = + new DefaultRepositorySystemSession(session.getRepositorySession()); + repositorySession.setAuthenticationSelector(authenticationSelector); + repositorySession.setProxySelector(proxySelector); + repositorySession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager( + repositorySession, new LocalRepository(isolatedLocalRepository.toFile()))); + when(session.getRepositorySession()).thenReturn(repositorySession); + } + /** * Test transitive parameter * @@ -126,15 +198,80 @@ void testRemoteRepositoriesAuthentication(GetMojo mojo) throws Exception { org.eclipse.jetty.server.Server server = createServer(); try { server.start(); - ServerConnector serverConnector = (ServerConnector) server.getConnectors()[0]; - String url = "http://" - + (serverConnector.getHost() == null - ? InetAddress.getLoopbackAddress().getHostName() - : serverConnector.getHost()); - url = url + ":" + serverConnector.getLocalPort() + "/maven"; + + setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + + applySettingsToRepositorySession(); + + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); + pbr.setRepositorySession(session.getRepositorySession()); + when(session.getProjectBuildingRequest()).thenReturn(pbr); + + mojo.setGroupId("test"); + mojo.setArtifactId("test"); + mojo.setVersion("1.0"); + + mojo.execute(); + } finally { + server.stop(); + } + } + + /** + * Test that an active proxy from the settings is applied to the repositories given by the + * remoteRepositories parameter. The proxy points at a host that cannot resolve, so a successful + * resolution would mean the proxy was never applied. + * + * @throws Exception in case of errors + */ + @Test + @InjectMojo(goal = "get") + void testRemoteRepositoriesProxy(GetMojo mojo) throws Exception { + org.eclipse.jetty.server.Server server = createServer(); + try { + server.start(); + + setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + + session.getSettings().addProxy(createProxy(null)); + applySettingsToRepositorySession(); + + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); + pbr.setRepositorySession(session.getRepositorySession()); + when(session.getProjectBuildingRequest()).thenReturn(pbr); + + mojo.setGroupId("test"); + mojo.setArtifactId("test"); + mojo.setVersion("1.0"); + + MojoExecutionException e = assertThrows(MojoExecutionException.class, mojo::execute); + assertTrue( + mentionsProxyHost(e), + "Expected the resolution to have been attempted through the unreachable proxy, got: " + e); + } finally { + server.stop(); + } + } + + /** + * Test that nonProxyHosts excludes a repository from the proxy that would otherwise match it: the + * same unreachable proxy as above must not be applied, so the resolution has to succeed. + * + * @throws Exception in case of errors + */ + @Test + @InjectMojo(goal = "get") + void testRemoteRepositoriesNonProxyHosts(GetMojo mojo) throws Exception { + org.eclipse.jetty.server.Server server = createServer(); + try { + server.start(); + String url = serverUrl(server); setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + url); + session.getSettings().addProxy(createProxy(URI.create(url).getHost())); + applySettingsToRepositorySession(); + DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); pbr.setRepositorySession(session.getRepositorySession()); when(session.getProjectBuildingRequest()).thenReturn(pbr); @@ -149,6 +286,41 @@ void testRemoteRepositoriesAuthentication(GetMojo mojo) throws Exception { } } + /** + * Whether the failure came from the proxy rather than from, say, a missing artifact or a rejected login — the + * host of {@link #createProxy(String)} only ever appears if the proxy really was applied to the repository. + */ + private boolean mentionsProxyHost(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause.getMessage() != null && cause.getMessage().contains(PROXY_HOST)) { + return true; + } + } + return false; + } + + /** + * An active HTTP proxy pointing at a name that is guaranteed not to resolve (RFC 2606 reserved TLD), so that + * applying it is always observable as a failure. + */ + private Proxy createProxy(String nonProxyHosts) { + Proxy proxy = new Proxy(); + proxy.setActive(true); + proxy.setProtocol("http"); + proxy.setHost(PROXY_HOST); + proxy.setPort(3128); + proxy.setNonProxyHosts(nonProxyHosts); + return proxy; + } + + private String serverUrl(org.eclipse.jetty.server.Server server) throws Exception { + ServerConnector serverConnector = (ServerConnector) server.getConnectors()[0]; + String host = serverConnector.getHost() == null + ? InetAddress.getLoopbackAddress().getHostName() + : serverConnector.getHost(); + return "http://" + host + ":" + serverConnector.getLocalPort() + "/maven"; + } + /** * Test parsing of the remote repositories parameter * diff --git a/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java b/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java new file mode 100644 index 000000000..22e7ec1b4 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java @@ -0,0 +1,184 @@ +/* + * 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.maven.plugins.dependency.utils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; +import org.apache.maven.artifact.repository.MavenArtifactRepository; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.util.repository.AuthenticationBuilder; +import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; +import org.eclipse.aether.util.repository.DefaultMirrorSelector; +import org.eclipse.aether.util.repository.DefaultProxySelector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Covers the mirror, proxy and authentication selection that {@link RepositorySessionInjector} takes over from + * {@code MavenRepositorySystem}, which a plugin cannot reference. + */ +class RepositorySessionInjectorTest { + + private RepositorySessionInjector injector; + + private DefaultRepositorySystemSession session; + + @BeforeEach + void setUp() { + Map layouts = new HashMap<>(); + layouts.put("default", new DefaultRepositoryLayout()); + injector = new RepositorySessionInjector(layouts); + session = new DefaultRepositorySystemSession(); + } + + private ArtifactRepository repository(String id, String url) { + return new MavenArtifactRepository( + id, url, new DefaultRepositoryLayout(), new ArtifactRepositoryPolicy(), new ArtifactRepositoryPolicy()); + } + + @Test + void nullSessionLeavesRepositoriesAlone() { + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + + injector.inject(null, Collections.singletonList(repository)); + + assertEquals("central", repository.getId()); + assertNull(repository.getAuthentication()); + assertNull(repository.getProxy()); + } + + @Test + void mirrorReplacesIdAndUrlAndKeepsTheOriginal() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertEquals("mirror", repository.getId()); + assertEquals("https://mirror.example.com", repository.getUrl()); + + List mirrored = repository.getMirroredRepositories(); + assertEquals(1, mirrored.size()); + assertEquals("central", mirrored.get(0).getId()); + assertEquals("https://repo.example.com/maven2", mirrored.get(0).getUrl()); + // MavenArtifactRepository stores null policies as null; the mirrored original must not carry any + assertNotNull(mirrored.get(0).getSnapshots()); + assertNotNull(mirrored.get(0).getReleases()); + } + + @Test + void mirrorOfDoesNotMatchEveryRepository() { + session.setMirrorSelector(new DefaultMirrorSelector() + .add("mirror", "https://mirror.example.com", null, false, false, "central", null)); + + ArtifactRepository repository = repository("other", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertEquals("other", repository.getId()); + assertEquals("https://repo.example.com/maven2", repository.getUrl()); + } + + @Test + void blockedMirrorBlocksTheRepository() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, true, "*", null)); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertTrue(repository.isBlocked()); + } + + @Test + void credentialsAreSelectedByRepositoryId() { + session.setAuthenticationSelector(new DefaultAuthenticationSelector() + .add( + "myserver", + new AuthenticationBuilder() + .addUsername("foo") + .addPassword("bar") + .build())); + + ArtifactRepository matching = repository("myserver", "https://repo.example.com/maven2"); + ArtifactRepository other = repository("other", "https://repo.example.com/maven2"); + injector.inject(session, java.util.Arrays.asList(matching, other)); + + assertNotNull(matching.getAuthentication()); + assertEquals("foo", matching.getAuthentication().getUsername()); + assertEquals("bar", matching.getAuthentication().getPassword()); + assertNull(other.getAuthentication()); + } + + @Test + void credentialsAreSelectedForTheMirrorNotTheMirroredRepository() { + session.setMirrorSelector( + new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); + session.setAuthenticationSelector(new DefaultAuthenticationSelector() + .add("mirror", new AuthenticationBuilder().addUsername("foo").build()) + .add("central", new AuthenticationBuilder().addUsername("wrong").build())); + + ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(repository)); + + assertNotNull(repository.getAuthentication()); + assertEquals("foo", repository.getAuthentication().getUsername()); + } + + @Test + void proxyIsSelectedByProtocol() { + session.setProxySelector(new DefaultProxySelector() + .add(new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), null)); + + ArtifactRepository http = repository("http-repo", "http://repo.example.com/maven2"); + injector.inject(session, Collections.singletonList(http)); + + assertNotNull(http.getProxy()); + assertEquals("proxy.example.com", http.getProxy().getHost()); + assertEquals(3128, http.getProxy().getPort()); + assertEquals("http", http.getProxy().getProtocol()); + } + + @Test + void nonProxyHostsExcludesTheRepository() { + session.setProxySelector(new DefaultProxySelector() + .add( + new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), + "*.example.com|localhost")); + + ArtifactRepository excluded = repository("repo", "http://repo.example.com/maven2"); + ArtifactRepository proxied = repository("repo", "http://repo.example.org/maven2"); + injector.inject(session, java.util.Arrays.asList(excluded, proxied)); + + assertNull(excluded.getProxy()); + assertNotNull(proxied.getProxy()); + } +} From f149b7f800b5904b3561b8405499d0b59dcb73e4 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 9 Aug 2026 19:54:22 +0200 Subject: [PATCH 2/2] [MDEP-858] Get rid of maven-artifact-transfer from the get goal Resolve through ResolverUtil instead of reimplementing repository session injection in the plugin. ResolverUtil.remoteRepositories() already parses the same id::layout::url syntax into Resolver's RemoteRepository and passes the result to RepositorySystem.newResolutionRepositories(), which applies the session's mirror, proxy and authentication selectors. That is the supported replacement for the legacy injectMirror/injectProxy/injectAuthentication calls -- and the same selectors Resolver consults when it performs the transfer -- so the RepositorySessionInjector added earlier in this branch, which reimplemented MavenRepositorySystem's session-based methods because org.apache.maven.bridge is not exported to plugin class realms, is not needed at all. This is the migration list-classes had in MDEP-924. It drops the goal's use of maven-artifact-transfer along with the deprecated Maven 3 repository model: ArtifactRepository, MavenArtifactRepository, ArtifactRepositoryPolicy, ArtifactRepositoryLayout and ProjectBuildingRequest. GetMojo now compiles without a deprecation warning. maven-artifact-transfer stays in the pom; purge-local-repository, copy-dependencies and ArtifactItem still use it. Three visible consequences, all shared with list-classes: remoteRepositories becomes a List rather than a comma-separated String, the repository syntax accepts id::url and no longer rejects an unknown layout id, and extra repositories follow the session's update policy rather than being forced to always. testParseRepository goes away with parseRepository; ResolverUtilTest covers the same syntax, and two tests replace the argument validation that stayed in the mojo. Generated-by: Claude Opus 5 (1M context) --- .../maven/plugins/dependency/GetMojo.java | 220 +++++------------- .../utils/RepositorySessionInjector.java | 193 --------------- .../maven/plugins/dependency/TestGetMojo.java | 115 +++------ .../utils/RepositorySessionInjectorTest.java | 184 --------------- 4 files changed, 86 insertions(+), 626 deletions(-) delete mode 100644 src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java delete mode 100644 src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java diff --git a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java index fafb54fe4..feb3f18da 100644 --- a/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java +++ b/src/main/java/org/apache/maven/plugins/dependency/GetMojo.java @@ -20,35 +20,20 @@ import javax.inject.Inject; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.maven.artifact.handler.ArtifactHandler; -import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; -import org.apache.maven.artifact.repository.ArtifactRepository; -import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; -import org.apache.maven.artifact.repository.MavenArtifactRepository; -import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; -import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.plugins.dependency.utils.RepositorySessionInjector; -import org.apache.maven.project.DefaultProjectBuildingRequest; -import org.apache.maven.project.ProjectBuildingRequest; -import org.apache.maven.shared.transfer.artifact.ArtifactCoordinate; -import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate; -import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver; -import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException; -import org.apache.maven.shared.transfer.dependencies.DefaultDependableCoordinate; -import org.apache.maven.shared.transfer.dependencies.DependableCoordinate; -import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver; -import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolverException; +import org.apache.maven.plugins.dependency.utils.ParamArtifact; +import org.apache.maven.plugins.dependency.utils.ResolverUtil; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactDescriptorException; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.DependencyResolutionException; /** * Resolves a single artifact, eventually transitively, from the specified remote repositories. Caveat: will always @@ -56,44 +41,19 @@ */ @Mojo(name = "get", requiresProject = false, threadSafe = true) public class GetMojo extends AbstractMojo { - private static final Pattern ALT_REPO_SYNTAX_PATTERN = Pattern.compile("(.+)::(.*)::(.+)"); - private final MavenSession session; + private final ResolverUtil resolverUtil; - private final ArtifactResolver artifactResolver; - - private final DependencyResolver dependencyResolver; - - private final ArtifactHandlerManager artifactHandlerManager; + private final ParamArtifact paramArtifact = new ParamArtifact(); /** - * Map that contains the layouts. - */ - private final Map repositoryLayouts; - - /** - * Applies the mirrors, proxies and credentials of the repository session to the repositories named by - * {@link #remoteRepositories}. - */ - private final RepositorySessionInjector repositorySessionInjector; - - private final DefaultDependableCoordinate coordinate = new DefaultDependableCoordinate(); - - /** - * Repositories in the format id::[layout]::url or just url, separated by comma. i.e. - * central::default::https://repo.maven.apache.org/maven2,myrepo::::https://repo.acme.com,https://repo.acme2.com. + * Repositories in the format {@code id::[layout::]url} or just URLs, separated by comma. That is, + * + * central::default::https://repo.maven.apache.org/maven2,myrepo::https://repo.acme.com,https://repo.acme2.com + * */ @Parameter(property = "remoteRepositories") - private String remoteRepositories; - - /** - * A string of the form groupId:artifactId:version[:packaging[:classifier]]. - */ - @Parameter(property = "artifact") - private String artifact; - - @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true) - private List pomRemoteRepositories; + private List remoteRepositories; /** * Resolve transitively, retrieving the specified artifact and all of its dependencies. @@ -110,18 +70,8 @@ public class GetMojo extends AbstractMojo { private boolean skip; @Inject - public GetMojo( - MavenSession session, - ArtifactResolver artifactResolver, - DependencyResolver dependencyResolver, - ArtifactHandlerManager artifactHandlerManager, - Map repositoryLayouts) { - this.session = session; - this.artifactResolver = artifactResolver; - this.dependencyResolver = dependencyResolver; - this.artifactHandlerManager = artifactHandlerManager; - this.repositoryLayouts = repositoryLayouts; - this.repositorySessionInjector = new RepositorySessionInjector(repositoryLayouts); + public GetMojo(ResolverUtil resolverUtil) { + this.resolverUtil = resolverUtil; } @Override @@ -131,110 +81,34 @@ public void execute() throws MojoExecutionException, MojoFailureException { return; } - if (coordinate.getArtifactId() == null && artifact == null) { - throw new MojoFailureException("You must specify an artifact, " - + "e.g. -Dartifact=org.apache.maven.plugins:maven-downloader-plugin:1.0"); - } - if (artifact != null) { - String[] tokens = artifact.split(":"); - if (tokens.length < 3 || tokens.length > 5) { - throw new MojoFailureException("Invalid artifact, you must specify " - + "groupId:artifactId:version[:packaging[:classifier]] " + artifact); - } - coordinate.setGroupId(tokens[0]); - coordinate.setArtifactId(tokens[1]); - coordinate.setVersion(tokens[2]); - if (tokens.length >= 4) { - coordinate.setType(tokens[3]); - } - if (tokens.length == 5) { - coordinate.setClassifier(tokens[4]); - } - } - - ArtifactRepositoryPolicy always = new ArtifactRepositoryPolicy( - true, ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS, ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN); - - List repoList = new ArrayList<>(); - - if (pomRemoteRepositories != null) { - repoList.addAll(pomRemoteRepositories); + if (!paramArtifact.isDataSet()) { + throw new MojoFailureException("You must specify an artifact OR GAV separately, " + + "e.g. -Dartifact=org.apache.maven.plugins:maven-downloader-plugin:1.0 OR " + + "-DgroupId=org.apache.maven.plugins -DartifactId=maven-downloader-plugin -Dversion=1.0"); } - if (remoteRepositories != null) { - // Use the same format as in the deploy plugin id::layout::url - String[] repos = remoteRepositories.split(","); - for (String repo : repos) { - repoList.add(parseRepository(repo, always)); - } + Artifact artifact; + List repositories; + try { + artifact = resolverUtil.createArtifactFromParams(paramArtifact); + repositories = resolverUtil.remoteRepositories(remoteRepositories); + } catch (IllegalArgumentException e) { + throw new MojoFailureException(e.getMessage(), e); } try { - ProjectBuildingRequest buildingRequest = - new DefaultProjectBuildingRequest(session.getProjectBuildingRequest()); - - repositorySessionInjector.inject(session.getRepositorySession(), repoList); - - buildingRequest.setRemoteRepositories(repoList); - if (transitive) { - getLog().info("Resolving " + coordinate + " with transitive dependencies"); - dependencyResolver.resolveDependencies(buildingRequest, coordinate, null); + getLog().info("Resolving " + artifact + " with transitive dependencies"); + resolverUtil.resolveDependencies(artifact, repositories); } else { - getLog().info("Resolving " + coordinate); - artifactResolver.resolveArtifact(buildingRequest, toArtifactCoordinate(coordinate)); + getLog().info("Resolving " + artifact); + resolverUtil.resolveArtifact(artifact, repositories); } - } catch (ArtifactResolverException | DependencyResolverException e) { + } catch (ArtifactResolutionException | ArtifactDescriptorException | DependencyResolutionException e) { throw new MojoExecutionException("Couldn't download artifact: " + e.getMessage(), e); } } - private ArtifactCoordinate toArtifactCoordinate(DependableCoordinate dependableCoordinate) { - ArtifactHandler artifactHandler = artifactHandlerManager.getArtifactHandler(dependableCoordinate.getType()); - DefaultArtifactCoordinate artifactCoordinate = new DefaultArtifactCoordinate(); - artifactCoordinate.setGroupId(dependableCoordinate.getGroupId()); - artifactCoordinate.setArtifactId(dependableCoordinate.getArtifactId()); - artifactCoordinate.setVersion(dependableCoordinate.getVersion()); - artifactCoordinate.setClassifier(dependableCoordinate.getClassifier()); - artifactCoordinate.setExtension(artifactHandler.getExtension()); - return artifactCoordinate; - } - - ArtifactRepository parseRepository(String repo, ArtifactRepositoryPolicy policy) throws MojoFailureException { - // if it's a simple url - String id = "temp"; - ArtifactRepositoryLayout layout = getLayout("default"); - String url = repo; - - // if it's an extended repo URL of the form id::layout::url - if (repo.contains("::")) { - Matcher matcher = ALT_REPO_SYNTAX_PATTERN.matcher(repo); - if (!matcher.matches()) { - throw new MojoFailureException( - repo, - "Invalid syntax for repository: " + repo, - "Invalid syntax for repository. Use \"id::layout::url\" or \"URL\"."); - } - - id = matcher.group(1).trim(); - if (matcher.group(2) != null && !matcher.group(2).isEmpty()) { - layout = getLayout(matcher.group(2).trim()); - } - url = matcher.group(3).trim(); - } - return new MavenArtifactRepository(id, url, layout, policy, policy); - } - - private ArtifactRepositoryLayout getLayout(String id) throws MojoFailureException { - ArtifactRepositoryLayout layout = repositoryLayouts.get(id); - - if (layout == null) { - throw new MojoFailureException(id, "Invalid repository layout", "Invalid repository layout: " + id); - } - - return layout; - } - /** * @return {@link #skip} */ @@ -243,53 +117,63 @@ protected boolean isSkip() { } /** - * The groupId of the artifact to resolve. Ignored if {@link #artifact} is used. + * The groupId of the artifact to resolve. Ignored if {@code artifact} is used. * * @param groupId the groupId */ @Parameter(property = "groupId") public void setGroupId(String groupId) { - this.coordinate.setGroupId(groupId); + paramArtifact.setGroupId(groupId); } /** - * The artifactId of the artifact to resolve. Ignored if {@link #artifact} is used. + * The artifactId of the artifact to resolve. Ignored if {@code artifact} is used. * * @param artifactId the artifactId */ @Parameter(property = "artifactId") public void setArtifactId(String artifactId) { - this.coordinate.setArtifactId(artifactId); + paramArtifact.setArtifactId(artifactId); } /** - * The version of the artifact to resolve. Ignored if {@link #artifact} is used. + * The version of the artifact to resolve. Ignored if {@code artifact} is used. * * @param version the version */ @Parameter(property = "version") public void setVersion(String version) { - this.coordinate.setVersion(version); + paramArtifact.setVersion(version); } /** - * The classifier of the artifact to resolve. Ignored if {@link #artifact} is used. + * The classifier of the artifact to resolve. Ignored if {@code artifact} is used. * * @param classifier the classifier to be used * @since 2.3 */ @Parameter(property = "classifier") public void setClassifier(String classifier) { - this.coordinate.setClassifier(classifier); + paramArtifact.setClassifier(classifier); } /** - * The packaging of the artifact to resolve. Ignored if {@link #artifact} is used. + * The packaging of the artifact to resolve. Ignored if {@code artifact} is used. * * @param type packaging */ @Parameter(property = "packaging", defaultValue = "jar") public void setPackaging(String type) { - this.coordinate.setType(type); + paramArtifact.setPackaging(type); + } + + /** + * A string of the form groupId:artifactId:version[:packaging[:classifier]]. + * + * @param artifact the artifact coordinates + */ + @Parameter(property = "artifact") + public void setArtifact(String artifact) { + paramArtifact.setArtifact(artifact); } } diff --git a/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java b/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java deleted file mode 100644 index e11a947f8..000000000 --- a/src/main/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjector.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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.maven.plugins.dependency.utils; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.apache.maven.RepositoryUtils; -import org.apache.maven.artifact.repository.ArtifactRepository; -import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; -import org.apache.maven.artifact.repository.Authentication; -import org.apache.maven.artifact.repository.MavenArtifactRepository; -import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; -import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout2; -import org.apache.maven.repository.Proxy; -import org.eclipse.aether.RepositorySystemSession; -import org.eclipse.aether.repository.AuthenticationContext; -import org.eclipse.aether.repository.AuthenticationSelector; -import org.eclipse.aether.repository.MirrorSelector; -import org.eclipse.aether.repository.ProxySelector; -import org.eclipse.aether.repository.RemoteRepository; - -/** - * Applies the mirror, proxy and authentication configuration of a {@link RepositorySystemSession} to repositories - * that were built by hand rather than obtained from the project, so that they can be used for resolution. - *

- * This is the work {@code org.apache.maven.bridge.MavenRepositorySystem} does in its session-based - * {@code injectMirror}/{@code injectProxy}/{@code injectAuthentication} methods, reimplemented here because - * {@code org.apache.maven.bridge} is not one of the packages maven-core exports to plugin class realms — a plugin - * that references it compiles and unit-tests cleanly and then fails at runtime with - * {@code NoClassDefFoundError: org/apache/maven/bridge/MavenRepositorySystem}. Everything used below is in a package - * maven-core does export: {@code org.apache.maven} (for {@link RepositoryUtils}), {@code org.apache.maven.artifact}, - * {@code org.apache.maven.repository} and {@code org.eclipse.aether.repository}. - *

- * The session's selectors are populated by Maven from the mirrors, proxies and servers of - * settings.xml, with the servers and proxies already decrypted, and they are the same selectors - * Resolver consults when it performs the transfer. - */ -public class RepositorySessionInjector { - - private final Map repositoryLayouts; - - public RepositorySessionInjector(Map repositoryLayouts) { - this.repositoryLayouts = repositoryLayouts; - } - - /** - * Applies the session's mirror, then proxy, then authentication configuration to each repository, in that order: - * mirroring rewrites the id and URL a repository is known by, and the proxy and the credentials are selected for - * the mirror rather than for the repository it replaced. - * - * @param session the repository session, may be {@code null} - * @param repositories the repositories to modify in place, may be {@code null} - */ - public void inject(RepositorySystemSession session, List repositories) { - if (session == null || repositories == null) { - return; - } - - for (ArtifactRepository repository : repositories) { - injectMirror(session, repository); - repository.setProxy(getProxy(session, repository)); - repository.setAuthentication(getAuthentication(session, repository)); - } - } - - private void injectMirror(RepositorySystemSession session, ArtifactRepository repository) { - MirrorSelector selector = session.getMirrorSelector(); - if (selector == null) { - return; - } - - RemoteRepository mirror = selector.getMirror(RepositoryUtils.toRepo(repository)); - if (mirror == null) { - return; - } - - repository.setMirroredRepositories(Collections.singletonList(createArtifactRepository( - repository.getId(), - repository.getUrl(), - repository.getLayout(), - repository.getSnapshots(), - repository.getReleases()))); - - repository.setId(mirror.getId()); - repository.setUrl(mirror.getUrl()); - - String layoutId = mirror.getContentType(); - if (layoutId != null && !layoutId.isEmpty()) { - repository.setLayout(getLayout(layoutId)); - } - - repository.setBlocked(mirror.isBlocked()); - } - - /** - * Same as {@code MavenRepositorySystem.createArtifactRepository}, including its substitution of default policies - * for null ones — {@link MavenArtifactRepository} stores null and fails later at whichever call site validates. - */ - private static ArtifactRepository createArtifactRepository( - String id, - String url, - ArtifactRepositoryLayout layout, - ArtifactRepositoryPolicy snapshots, - ArtifactRepositoryPolicy releases) { - ArtifactRepositoryPolicy snapshotPolicy = snapshots != null ? snapshots : new ArtifactRepositoryPolicy(); - ArtifactRepositoryPolicy releasePolicy = releases != null ? releases : new ArtifactRepositoryPolicy(); - - if (layout instanceof ArtifactRepositoryLayout2) { - return ((ArtifactRepositoryLayout2) layout) - .newMavenArtifactRepository(id, url, snapshotPolicy, releasePolicy); - } - return new MavenArtifactRepository(id, url, layout, snapshotPolicy, releasePolicy); - } - - /** - * Resolves a mirror's layout id. Maven core wraps an unknown id in a layout that delegates to the default one; - * falling back to the default layout outright computes the same paths. - */ - private ArtifactRepositoryLayout getLayout(String layoutId) { - ArtifactRepositoryLayout layout = repositoryLayouts.get(layoutId); - return layout != null ? layout : repositoryLayouts.get("default"); - } - - private Proxy getProxy(RepositorySystemSession session, ArtifactRepository repository) { - ProxySelector selector = session.getProxySelector(); - if (selector == null) { - return null; - } - - RemoteRepository repo = RepositoryUtils.toRepo(repository); - org.eclipse.aether.repository.Proxy proxy = selector.getProxy(repo); - if (proxy == null) { - return null; - } - - Proxy result = new Proxy(); - result.setHost(proxy.getHost()); - result.setProtocol(proxy.getType()); - result.setPort(proxy.getPort()); - - if (proxy.getAuthentication() != null) { - repo = new RemoteRepository.Builder(repo).setProxy(proxy).build(); - try (AuthenticationContext authCtx = AuthenticationContext.forProxy(session, repo)) { - result.setUserName(authCtx.get(AuthenticationContext.USERNAME)); - result.setPassword(authCtx.get(AuthenticationContext.PASSWORD)); - result.setNtlmDomain(authCtx.get(AuthenticationContext.NTLM_DOMAIN)); - result.setNtlmHost(authCtx.get(AuthenticationContext.NTLM_WORKSTATION)); - } - } - - return result; - } - - private Authentication getAuthentication(RepositorySystemSession session, ArtifactRepository repository) { - AuthenticationSelector selector = session.getAuthenticationSelector(); - if (selector == null) { - return null; - } - - RemoteRepository repo = RepositoryUtils.toRepo(repository); - org.eclipse.aether.repository.Authentication auth = selector.getAuthentication(repo); - if (auth == null) { - return null; - } - - repo = new RemoteRepository.Builder(repo).setAuthentication(auth).build(); - try (AuthenticationContext authCtx = AuthenticationContext.forRepository(session, repo)) { - Authentication result = new Authentication( - authCtx.get(AuthenticationContext.USERNAME), authCtx.get(AuthenticationContext.PASSWORD)); - result.setPrivateKey(authCtx.get(AuthenticationContext.PRIVATE_KEY_PATH)); - result.setPassphrase(authCtx.get(AuthenticationContext.PRIVATE_KEY_PASSPHRASE)); - return result; - } - } -} diff --git a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java index 028500278..f1e905029 100644 --- a/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java +++ b/src/test/java/org/apache/maven/plugins/dependency/TestGetMojo.java @@ -23,19 +23,16 @@ import java.net.InetAddress; import java.net.URI; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import org.apache.maven.api.plugin.testing.Basedir; import org.apache.maven.api.plugin.testing.InjectMojo; import org.apache.maven.api.plugin.testing.MojoParameter; import org.apache.maven.api.plugin.testing.MojoTest; -import org.apache.maven.artifact.repository.ArtifactRepository; -import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; -import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.settings.Proxy; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; @@ -60,10 +57,8 @@ import static org.apache.maven.api.plugin.testing.MojoExtension.getTestPath; import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.when; @MojoTest(realRepositorySession = true) @@ -95,10 +90,10 @@ void setUp() { /** * Mirrors the {@code } and {@code } entries of the session settings into the repository session, - * the way {@code DefaultRepositorySystemSessionFactory} does when Maven builds that session for real. GetMojo - * reads credentials and proxies from the session's selectors rather than from the {@link Settings} object, since - * those selectors are what Resolver itself consults when it performs the transfer, and the test harness has - * already created the repository session by the time {@link #setUp()} declares any of this. + * the way {@code DefaultRepositorySystemSessionFactory} does when Maven builds that session for real. The + * repositories named by the {@code remoteRepositories} parameter get their credentials and proxies from those + * selectors, by way of {@code RepositorySystem.newResolutionRepositories}, and the test harness has already + * created the repository session by the time {@link #setUp()} declares any of this. *

* The local repository is swapped for an empty one at the same time, so that the tests below depend on the * transfer actually happening rather than on what an earlier test left in the shared local repository. @@ -153,10 +148,6 @@ private void applySettingsToRepositorySession() { @InjectMojo(goal = "get") @MojoParameter(name = "transitive", value = "false") void testTransitive(GetMojo mojo) throws Exception { - DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); - pbr.setRepositorySession(session.getRepositorySession()); - when(session.getProjectBuildingRequest()).thenReturn(pbr); - mojo.setGroupId("org.apache.maven"); mojo.setArtifactId("maven-model"); mojo.setVersion("2.0.9"); @@ -173,13 +164,10 @@ void testTransitive(GetMojo mojo) throws Exception { @InjectMojo(goal = "get") @MojoParameter( name = "remoteRepositories", - value = - "central::default::https://repo.maven.apache.org/maven2,central::::https://repo.maven.apache.org/maven2,https://repo.maven.apache.org/maven2") + value = "central::default::https://repo.maven.apache.org/maven2," + + "central::::https://repo.maven.apache.org/maven2," + + "https://repo.maven.apache.org/maven2") void testRemoteRepositories(GetMojo mojo) throws Exception { - DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); - pbr.setRepositorySession(session.getRepositorySession()); - when(session.getProjectBuildingRequest()).thenReturn(pbr); - mojo.setGroupId("org.apache.maven"); mojo.setArtifactId("maven-model"); mojo.setVersion("2.0.9"); @@ -187,6 +175,25 @@ void testRemoteRepositories(GetMojo mojo) throws Exception { mojo.execute(); } + /** + * Test that neither an artifact nor a complete GAV is a failure rather than an attempted resolution. + */ + @Test + @InjectMojo(goal = "get") + void testMissingArtifact(GetMojo mojo) { + assertThrows(MojoFailureException.class, mojo::execute); + } + + /** + * Test that a malformed artifact parameter is reported as a failure. + */ + @Test + @InjectMojo(goal = "get") + @MojoParameter(name = "artifact", value = "org.apache.maven:maven-model") + void testInvalidArtifact(GetMojo mojo) { + assertThrows(MojoFailureException.class, mojo::execute); + } + /** * Test remote repositories parameter with basic authentication * @@ -199,14 +206,10 @@ void testRemoteRepositoriesAuthentication(GetMojo mojo) throws Exception { try { server.start(); - setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + setRemoteRepositories(mojo, "myserver::default::" + serverUrl(server)); applySettingsToRepositorySession(); - DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); - pbr.setRepositorySession(session.getRepositorySession()); - when(session.getProjectBuildingRequest()).thenReturn(pbr); - mojo.setGroupId("test"); mojo.setArtifactId("test"); mojo.setVersion("1.0"); @@ -231,15 +234,11 @@ void testRemoteRepositoriesProxy(GetMojo mojo) throws Exception { try { server.start(); - setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + serverUrl(server)); + setRemoteRepositories(mojo, "myserver::default::" + serverUrl(server)); session.getSettings().addProxy(createProxy(null)); applySettingsToRepositorySession(); - DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); - pbr.setRepositorySession(session.getRepositorySession()); - when(session.getProjectBuildingRequest()).thenReturn(pbr); - mojo.setGroupId("test"); mojo.setArtifactId("test"); mojo.setVersion("1.0"); @@ -267,15 +266,11 @@ void testRemoteRepositoriesNonProxyHosts(GetMojo mojo) throws Exception { server.start(); String url = serverUrl(server); - setVariableValueToObject(mojo, "remoteRepositories", "myserver::default::" + url); + setRemoteRepositories(mojo, "myserver::default::" + url); session.getSettings().addProxy(createProxy(URI.create(url).getHost())); applySettingsToRepositorySession(); - DefaultProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(); - pbr.setRepositorySession(session.getRepositorySession()); - when(session.getProjectBuildingRequest()).thenReturn(pbr); - mojo.setGroupId("test"); mojo.setArtifactId("test"); mojo.setVersion("1.0"); @@ -286,6 +281,10 @@ void testRemoteRepositoriesNonProxyHosts(GetMojo mojo) throws Exception { } } + private void setRemoteRepositories(GetMojo mojo, String... repositories) throws Exception { + setVariableValueToObject(mojo, "remoteRepositories", Arrays.asList(repositories)); + } + /** * Whether the failure came from the proxy rather than from, say, a missing artifact or a rejected login — the * host of {@link #createProxy(String)} only ever appears if the proxy really was applied to the repository. @@ -321,52 +320,6 @@ private String serverUrl(org.eclipse.jetty.server.Server server) throws Exceptio return "http://" + host + ":" + serverConnector.getLocalPort() + "/maven"; } - /** - * Test parsing of the remote repositories parameter - * - * @throws Exception in case of errors - */ - @Test - @InjectMojo(goal = "get") - void testParseRepository(GetMojo mojo) throws Exception { - ArtifactRepositoryPolicy policy = null; - ArtifactRepository repo = - mojo.parseRepository("central::default::https://repo.maven.apache.org/maven2", policy); - assertEquals("central", repo.getId()); - assertEquals(DefaultRepositoryLayout.class, repo.getLayout().getClass()); - assertEquals("https://repo.maven.apache.org/maven2", repo.getUrl()); - - try { - repo = mojo.parseRepository("central::legacy::https://repo.maven.apache.org/maven2", policy); - fail("Exception expected: legacy repository not supported anymore"); - } catch (MojoFailureException e) { - } - - repo = mojo.parseRepository("central::::https://repo.maven.apache.org/maven2", policy); - assertEquals("central", repo.getId()); - assertEquals(DefaultRepositoryLayout.class, repo.getLayout().getClass()); - assertEquals("https://repo.maven.apache.org/maven2", repo.getUrl()); - - repo = mojo.parseRepository("https://repo.maven.apache.org/maven2", policy); - assertEquals("temp", repo.getId()); - assertEquals(DefaultRepositoryLayout.class, repo.getLayout().getClass()); - assertEquals("https://repo.maven.apache.org/maven2", repo.getUrl()); - - try { - mojo.parseRepository("::::https://repo.maven.apache.org/maven2", policy); - fail("Exception expected"); - } catch (MojoFailureException e) { - // expected - } - - try { - mojo.parseRepository("central::https://repo.maven.apache.org/maven2", policy); - fail("Exception expected"); - } catch (MojoFailureException e) { - // expected - } - } - private ContextHandler createContextHandler() { ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setResourceBase(getTestPath("repository")); diff --git a/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java b/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java deleted file mode 100644 index 22e7ec1b4..000000000 --- a/src/test/java/org/apache/maven/plugins/dependency/utils/RepositorySessionInjectorTest.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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.maven.plugins.dependency.utils; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.maven.artifact.repository.ArtifactRepository; -import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; -import org.apache.maven.artifact.repository.MavenArtifactRepository; -import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; -import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; -import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.util.repository.AuthenticationBuilder; -import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; -import org.eclipse.aether.util.repository.DefaultMirrorSelector; -import org.eclipse.aether.util.repository.DefaultProxySelector; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Covers the mirror, proxy and authentication selection that {@link RepositorySessionInjector} takes over from - * {@code MavenRepositorySystem}, which a plugin cannot reference. - */ -class RepositorySessionInjectorTest { - - private RepositorySessionInjector injector; - - private DefaultRepositorySystemSession session; - - @BeforeEach - void setUp() { - Map layouts = new HashMap<>(); - layouts.put("default", new DefaultRepositoryLayout()); - injector = new RepositorySessionInjector(layouts); - session = new DefaultRepositorySystemSession(); - } - - private ArtifactRepository repository(String id, String url) { - return new MavenArtifactRepository( - id, url, new DefaultRepositoryLayout(), new ArtifactRepositoryPolicy(), new ArtifactRepositoryPolicy()); - } - - @Test - void nullSessionLeavesRepositoriesAlone() { - ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); - - injector.inject(null, Collections.singletonList(repository)); - - assertEquals("central", repository.getId()); - assertNull(repository.getAuthentication()); - assertNull(repository.getProxy()); - } - - @Test - void mirrorReplacesIdAndUrlAndKeepsTheOriginal() { - session.setMirrorSelector( - new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); - - ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); - injector.inject(session, Collections.singletonList(repository)); - - assertEquals("mirror", repository.getId()); - assertEquals("https://mirror.example.com", repository.getUrl()); - - List mirrored = repository.getMirroredRepositories(); - assertEquals(1, mirrored.size()); - assertEquals("central", mirrored.get(0).getId()); - assertEquals("https://repo.example.com/maven2", mirrored.get(0).getUrl()); - // MavenArtifactRepository stores null policies as null; the mirrored original must not carry any - assertNotNull(mirrored.get(0).getSnapshots()); - assertNotNull(mirrored.get(0).getReleases()); - } - - @Test - void mirrorOfDoesNotMatchEveryRepository() { - session.setMirrorSelector(new DefaultMirrorSelector() - .add("mirror", "https://mirror.example.com", null, false, false, "central", null)); - - ArtifactRepository repository = repository("other", "https://repo.example.com/maven2"); - injector.inject(session, Collections.singletonList(repository)); - - assertEquals("other", repository.getId()); - assertEquals("https://repo.example.com/maven2", repository.getUrl()); - } - - @Test - void blockedMirrorBlocksTheRepository() { - session.setMirrorSelector( - new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, true, "*", null)); - - ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); - injector.inject(session, Collections.singletonList(repository)); - - assertTrue(repository.isBlocked()); - } - - @Test - void credentialsAreSelectedByRepositoryId() { - session.setAuthenticationSelector(new DefaultAuthenticationSelector() - .add( - "myserver", - new AuthenticationBuilder() - .addUsername("foo") - .addPassword("bar") - .build())); - - ArtifactRepository matching = repository("myserver", "https://repo.example.com/maven2"); - ArtifactRepository other = repository("other", "https://repo.example.com/maven2"); - injector.inject(session, java.util.Arrays.asList(matching, other)); - - assertNotNull(matching.getAuthentication()); - assertEquals("foo", matching.getAuthentication().getUsername()); - assertEquals("bar", matching.getAuthentication().getPassword()); - assertNull(other.getAuthentication()); - } - - @Test - void credentialsAreSelectedForTheMirrorNotTheMirroredRepository() { - session.setMirrorSelector( - new DefaultMirrorSelector().add("mirror", "https://mirror.example.com", null, false, false, "*", null)); - session.setAuthenticationSelector(new DefaultAuthenticationSelector() - .add("mirror", new AuthenticationBuilder().addUsername("foo").build()) - .add("central", new AuthenticationBuilder().addUsername("wrong").build())); - - ArtifactRepository repository = repository("central", "https://repo.example.com/maven2"); - injector.inject(session, Collections.singletonList(repository)); - - assertNotNull(repository.getAuthentication()); - assertEquals("foo", repository.getAuthentication().getUsername()); - } - - @Test - void proxyIsSelectedByProtocol() { - session.setProxySelector(new DefaultProxySelector() - .add(new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), null)); - - ArtifactRepository http = repository("http-repo", "http://repo.example.com/maven2"); - injector.inject(session, Collections.singletonList(http)); - - assertNotNull(http.getProxy()); - assertEquals("proxy.example.com", http.getProxy().getHost()); - assertEquals(3128, http.getProxy().getPort()); - assertEquals("http", http.getProxy().getProtocol()); - } - - @Test - void nonProxyHostsExcludesTheRepository() { - session.setProxySelector(new DefaultProxySelector() - .add( - new org.eclipse.aether.repository.Proxy("http", "proxy.example.com", 3128), - "*.example.com|localhost")); - - ArtifactRepository excluded = repository("repo", "http://repo.example.com/maven2"); - ArtifactRepository proxied = repository("repo", "http://repo.example.org/maven2"); - injector.inject(session, java.util.Arrays.asList(excluded, proxied)); - - assertNull(excluded.getProxy()); - assertNotNull(proxied.getProxy()); - } -}