From d96c0848f8505b763e541729b642b069fef45735 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Thu, 14 Dec 2023 17:37:22 +0100 Subject: [PATCH] [MRESOLVER-455] Streaming bodies must be closed (#398) And JDK transport did not close them in case of errors. --- https://issues.apache.org/jira/browse/MRESOLVER-455 --- .github/workflows/maven-verify.yml | 2 +- Jenkinsfile | 2 +- .../internal/test/util/http/HttpServer.java | 34 +- .../test/util/http/HttpTransporterTest.java | 45 +- .../util/TestRepositorySystemSession.java | 818 ++++++++++++++++++ .../aether/transport/jdk/JdkTransporter.java | 192 ++-- .../transport/jdk/JdkTransporterCloser.java | 41 + .../maven-resolver-transport-jdk-21/pom.xml | 114 +++ .../transport/jdk/JdkTransporterCloser.java | 32 + .../maven-resolver-transport-jdk/pom.xml | 25 + maven-resolver-transport-jdk-parent/pom.xml | 1 + pom.xml | 2 +- 12 files changed, 1202 insertions(+), 106 deletions(-) create mode 100644 maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestRepositorySystemSession.java create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/pom.xml create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 2c5da7ff6..437a03d7f 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -28,5 +28,5 @@ jobs: with: ff-run: false ff-site-run: false - jdk-matrix: '[ "17", "21" ]' + jdk-matrix: '[ "21" ]' diff --git a/Jenkinsfile b/Jenkinsfile index a6a4e6469..27f098524 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,5 +17,5 @@ * under the License. */ -asfMavenTlpStdBuild( 'jdks' : [ "17", "21" ] ) +asfMavenTlpStdBuild( 'jdks' : [ "21" ] ) diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java index be684b149..919f33951 100644 --- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpServer.java @@ -21,10 +21,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; @@ -125,6 +122,8 @@ public enum ChecksumHeader { private final AtomicInteger connectionsToClose = new AtomicInteger(0); + private final AtomicInteger serverErrorsBeforeWorks = new AtomicInteger(0); + private final List logEntries = Collections.synchronizedList(new ArrayList<>()); public String getHost() { @@ -245,6 +244,11 @@ public HttpServer setConnectionsToClose(int connectionsToClose) { return this; } + public HttpServer setServerErrorsBeforeWorks(int serverErrorsBeforeWorks) { + this.serverErrorsBeforeWorks.set(serverErrorsBeforeWorks); + return this; + } + public HttpServer start() throws Exception { if (server != null) { return this; @@ -252,6 +256,7 @@ public HttpServer start() throws Exception { HandlerList handlers = new HandlerList(); handlers.addHandler(new ConnectionClosingHandler()); + handlers.addHandler(new ServerErrorHandler()); handlers.addHandler(new LogHandler()); handlers.addHandler(new ProxyAuthHandler()); handlers.addHandler(new AuthHandler()); @@ -286,6 +291,17 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } } + private class ServerErrorHandler extends AbstractHandler { + @Override + public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (serverErrorsBeforeWorks.getAndDecrement() > 0) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + writeResponseBodyMessage(response, "Oops, come back later!"); + } + } + } + private class LogHandler extends AbstractHandler { @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { @@ -326,6 +342,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS if (ExpectContinue.FAIL.equals(expectContinue) && request.getHeader(HttpHeader.EXPECT.asString()) != null) { response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); + writeResponseBodyMessage(response, "Expectation was set to fail"); return; } @@ -333,11 +350,13 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS if (HttpMethod.GET.is(req.getMethod()) || HttpMethod.HEAD.is(req.getMethod())) { if (!file.isFile() || path.endsWith(URIUtil.SLASH)) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); + writeResponseBodyMessage(response, "Not found"); return; } long ifUnmodifiedSince = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString()); if (ifUnmodifiedSince != -1L && file.lastModified() > ifUnmodifiedSince) { response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + writeResponseBodyMessage(response, "Precondition failed"); return; } long offset = 0L; @@ -348,6 +367,7 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS offset = Long.parseLong(m.group(1)); if (offset >= file.length()) { response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + writeResponseBodyMessage(response, "Range not satisfiable"); return; } } @@ -447,6 +467,12 @@ public void handle(String target, Request req, HttpServletRequest request, HttpS } } + private void writeResponseBodyMessage(HttpServletResponse response, String message) throws IOException { + try (OutputStream outputStream = response.getOutputStream()) { + outputStream.write(message.getBytes(StandardCharsets.UTF_8)); + } + } + private class RedirectHandler extends AbstractHandler { @Override public void handle(String target, Request req, HttpServletRequest request, HttpServletResponse response) { diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java index 4c82cb75f..31b4e44c3 100644 --- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java +++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java @@ -34,10 +34,10 @@ import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.DefaultRepositoryCache; -import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.DefaultSessionData; import org.eclipse.aether.internal.test.util.TestFileUtils; -import org.eclipse.aether.internal.test.util.TestUtils; +import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager; +import org.eclipse.aether.internal.test.util.TestRepositorySystemSession; import org.eclipse.aether.repository.Authentication; import org.eclipse.aether.repository.Proxy; import org.eclipse.aether.repository.RemoteRepository; @@ -84,12 +84,14 @@ public class HttpTransporterTest { private final Supplier transporterFactorySupplier; - protected DefaultRepositorySystemSession session; + protected TestRepositorySystemSession session; protected HttpTransporterFactory factory; protected HttpTransporter transporter; + protected Runnable closer; + protected File repoDir; protected HttpServer httpServer; @@ -134,9 +136,11 @@ protected void newTransporter(String url) throws Exception { transporter.close(); transporter = null; } - // here we "simulate" onSessionClose - // TODO: in UTs currently we cannot do it, sort it out - session = new DefaultRepositorySystemSession(session); + if (closer != null) { + closer.run(); + closer = null; + } + session = new TestRepositorySystemSession(session); session.setData(new DefaultSessionData()); transporter = factory.newInstance(session, newRepo(url)); } @@ -146,7 +150,8 @@ protected void newTransporter(String url) throws Exception { @BeforeEach protected void setUp(TestInfo testInfo) throws Exception { System.out.println("=== " + testInfo.getDisplayName() + " ==="); - session = TestUtils.newSession(); + session = new TestRepositorySystemSession(h -> this.closer = h); + session.setLocalRepositoryManager(new TestLocalRepositoryManager()); factory = transporterFactorySupplier.get(); repoDir = TestFileUtils.createTempDir(); TestFileUtils.writeString(new File(repoDir, "file.txt"), "test"); @@ -167,6 +172,10 @@ protected void tearDown() throws Exception { transporter.close(); transporter = null; } + if (closer != null) { + closer.run(); + closer = null; + } if (httpServer != null) { httpServer.stop(); httpServer = null; @@ -454,6 +463,28 @@ protected void testGet_SSL() throws Exception { assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); } + @Test + protected void testGet_SSL_WithServerErrors() throws Exception { + httpServer.setServerErrorsBeforeWorks(1); + httpServer.addSslConnector(); + newTransporter(httpServer.getHttpsUrl()); + for (int i = 1; i < 3; i++) { + try { + RecordingTransportListener listener = new RecordingTransportListener(); + GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener); + transporter.get(task); + assertEquals("test", task.getDataString()); + assertEquals(0L, listener.getDataOffset()); + assertEquals(4L, listener.getDataLength()); + assertEquals(1, listener.getStartedCount()); + assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount()); + assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8)); + } catch (HttpTransporterException e) { + assertEquals(500, e.getStatusCode()); + } + } + } + @Test protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception { session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown"); diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestRepositorySystemSession.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestRepositorySystemSession.java new file mode 100644 index 000000000..3c9e52c57 --- /dev/null +++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestRepositorySystemSession.java @@ -0,0 +1,818 @@ +/* + * 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.eclipse.aether.internal.test.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.aether.*; +import org.eclipse.aether.artifact.ArtifactType; +import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.collection.*; +import org.eclipse.aether.repository.*; +import org.eclipse.aether.resolution.ArtifactDescriptorPolicy; +import org.eclipse.aether.resolution.ResolutionErrorPolicy; +import org.eclipse.aether.transfer.TransferListener; + +import static java.util.Objects.requireNonNull; + +/** + * Test utility to create root sessions. + */ +public final class TestRepositorySystemSession implements RepositorySystemSession { + private boolean readOnly; + + private boolean offline; + + private boolean ignoreArtifactDescriptorRepositories; + + private ResolutionErrorPolicy resolutionErrorPolicy; + + private ArtifactDescriptorPolicy artifactDescriptorPolicy; + + private String checksumPolicy; + + private String artifactUpdatePolicy; + + private String metadataUpdatePolicy; + + private LocalRepositoryManager localRepositoryManager; + + private WorkspaceReader workspaceReader; + + private RepositoryListener repositoryListener; + + private TransferListener transferListener; + + private Map systemProperties; + + private Map systemPropertiesView; + + private Map userProperties; + + private Map userPropertiesView; + + private Map configProperties; + + private Map configPropertiesView; + + private MirrorSelector mirrorSelector; + + private ProxySelector proxySelector; + + private AuthenticationSelector authenticationSelector; + + private ArtifactTypeRegistry artifactTypeRegistry; + + private DependencyTraverser dependencyTraverser; + + private DependencyManager dependencyManager; + + private DependencySelector dependencySelector; + + private VersionFilter versionFilter; + + private DependencyGraphTransformer dependencyGraphTransformer; + + private SessionData data; + + private RepositoryCache cache; + + private final Function onSessionEndedRegistrar; + + public TestRepositorySystemSession(Consumer onSessionCloseConsumer) { + systemProperties = new HashMap<>(); + systemPropertiesView = Collections.unmodifiableMap(systemProperties); + userProperties = new HashMap<>(); + userPropertiesView = Collections.unmodifiableMap(userProperties); + configProperties = new HashMap<>(); + configPropertiesView = Collections.unmodifiableMap(configProperties); + mirrorSelector = NullMirrorSelector.INSTANCE; + proxySelector = NullProxySelector.INSTANCE; + authenticationSelector = NullAuthenticationSelector.INSTANCE; + artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE; + data = new DefaultSessionData(); + onSessionEndedRegistrar = h -> { + if (onSessionCloseConsumer != null) { + onSessionCloseConsumer.accept(h); + return true; + } else { + return false; + } + }; + } + + public TestRepositorySystemSession(RepositorySystemSession session) { + requireNonNull(session, "repository system session cannot be null"); + + setOffline(session.isOffline()); + setIgnoreArtifactDescriptorRepositories(session.isIgnoreArtifactDescriptorRepositories()); + setResolutionErrorPolicy(session.getResolutionErrorPolicy()); + setArtifactDescriptorPolicy(session.getArtifactDescriptorPolicy()); + setChecksumPolicy(session.getChecksumPolicy()); + setUpdatePolicy(session.getUpdatePolicy()); + setMetadataUpdatePolicy(session.getMetadataUpdatePolicy()); + setLocalRepositoryManager(session.getLocalRepositoryManager()); + setWorkspaceReader(session.getWorkspaceReader()); + setRepositoryListener(session.getRepositoryListener()); + setTransferListener(session.getTransferListener()); + setSystemProperties(session.getSystemProperties()); + setUserProperties(session.getUserProperties()); + setConfigProperties(session.getConfigProperties()); + setMirrorSelector(session.getMirrorSelector()); + setProxySelector(session.getProxySelector()); + setAuthenticationSelector(session.getAuthenticationSelector()); + setArtifactTypeRegistry(session.getArtifactTypeRegistry()); + setDependencyTraverser(session.getDependencyTraverser()); + setDependencyManager(session.getDependencyManager()); + setDependencySelector(session.getDependencySelector()); + setVersionFilter(session.getVersionFilter()); + setDependencyGraphTransformer(session.getDependencyGraphTransformer()); + setData(session.getData()); + setCache(session.getCache()); + this.onSessionEndedRegistrar = session::addOnSessionEndedHandler; + } + + @Override + public boolean isOffline() { + return offline; + } + + /** + * Controls whether the repository system operates in offline mode and avoids/refuses any access to remote + * repositories. + * + * @param offline {@code true} if the repository system is in offline mode, {@code false} otherwise. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setOffline(boolean offline) { + verifyStateForMutation(); + this.offline = offline; + return this; + } + + @Override + public boolean isIgnoreArtifactDescriptorRepositories() { + return ignoreArtifactDescriptorRepositories; + } + + /** + * Controls whether repositories declared in artifact descriptors should be ignored during transitive dependency + * collection. If enabled, only the repositories originally provided with the collect request will be considered. + * + * @param ignoreArtifactDescriptorRepositories {@code true} to ignore additional repositories from artifact + * descriptors, {@code false} to merge those with the originally + * specified repositories. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setIgnoreArtifactDescriptorRepositories( + boolean ignoreArtifactDescriptorRepositories) { + verifyStateForMutation(); + this.ignoreArtifactDescriptorRepositories = ignoreArtifactDescriptorRepositories; + return this; + } + + @Override + public ResolutionErrorPolicy getResolutionErrorPolicy() { + return resolutionErrorPolicy; + } + + /** + * Sets the policy which controls whether resolutions errors from remote repositories should be cached. + * + * @param resolutionErrorPolicy The resolution error policy for this session, may be {@code null} if resolution + * errors should generally not be cached. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setResolutionErrorPolicy(ResolutionErrorPolicy resolutionErrorPolicy) { + verifyStateForMutation(); + this.resolutionErrorPolicy = resolutionErrorPolicy; + return this; + } + + @Override + public ArtifactDescriptorPolicy getArtifactDescriptorPolicy() { + return artifactDescriptorPolicy; + } + + /** + * Sets the policy which controls how errors related to reading artifact descriptors should be handled. + * + * @param artifactDescriptorPolicy The descriptor error policy for this session, may be {@code null} if descriptor + * errors should generally not be tolerated. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setArtifactDescriptorPolicy(ArtifactDescriptorPolicy artifactDescriptorPolicy) { + verifyStateForMutation(); + this.artifactDescriptorPolicy = artifactDescriptorPolicy; + return this; + } + + @Override + public String getChecksumPolicy() { + return checksumPolicy; + } + + /** + * Sets the global checksum policy. If set, the global checksum policy overrides the checksum policies of the remote + * repositories being used for resolution. + * + * @param checksumPolicy The global checksum policy, may be {@code null}/empty to apply the per-repository policies. + * @return This session for chaining, never {@code null}. + * @see RepositoryPolicy#CHECKSUM_POLICY_FAIL + * @see RepositoryPolicy#CHECKSUM_POLICY_IGNORE + * @see RepositoryPolicy#CHECKSUM_POLICY_WARN + */ + public TestRepositorySystemSession setChecksumPolicy(String checksumPolicy) { + verifyStateForMutation(); + this.checksumPolicy = checksumPolicy; + return this; + } + + @Override + public String getUpdatePolicy() { + return getArtifactUpdatePolicy(); + } + + /** + * Sets the global update policy. If set, the global update policy overrides the update policies of the remote + * repositories being used for resolution. + *

+ * This method is meant for code that does not want to distinguish between artifact and metadata policies. + * Note: applications should either use get/set updatePolicy (this method and + * {@link RepositorySystemSession#getUpdatePolicy()}) or also distinguish between artifact and + * metadata update policies (and use other methods), but should not mix the two! + * + * @param updatePolicy The global update policy, may be {@code null}/empty to apply the per-repository policies. + * @return This session for chaining, never {@code null}. + * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS + * @see RepositoryPolicy#UPDATE_POLICY_DAILY + * @see RepositoryPolicy#UPDATE_POLICY_NEVER + * @see #setArtifactUpdatePolicy(String) + * @see #setMetadataUpdatePolicy(String) + */ + public TestRepositorySystemSession setUpdatePolicy(String updatePolicy) { + verifyStateForMutation(); + setArtifactUpdatePolicy(updatePolicy); + setMetadataUpdatePolicy(updatePolicy); + return this; + } + + @Override + public String getArtifactUpdatePolicy() { + return artifactUpdatePolicy; + } + + /** + * Sets the global artifact update policy. If set, the global update policy overrides the artifact update policies + * of the remote repositories being used for resolution. + * + * @param artifactUpdatePolicy The global update policy, may be {@code null}/empty to apply the per-repository policies. + * @return This session for chaining, never {@code null}. + * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS + * @see RepositoryPolicy#UPDATE_POLICY_DAILY + * @see RepositoryPolicy#UPDATE_POLICY_NEVER + * @since 2.0.0 + */ + public TestRepositorySystemSession setArtifactUpdatePolicy(String artifactUpdatePolicy) { + verifyStateForMutation(); + this.artifactUpdatePolicy = artifactUpdatePolicy; + return this; + } + + @Override + public String getMetadataUpdatePolicy() { + return metadataUpdatePolicy; + } + + /** + * Sets the global metadata update policy. If set, the global update policy overrides the metadata update policies + * of the remote repositories being used for resolution. + * + * @param metadataUpdatePolicy The global update policy, may be {@code null}/empty to apply the per-repository policies. + * @return This session for chaining, never {@code null}. + * @see RepositoryPolicy#UPDATE_POLICY_ALWAYS + * @see RepositoryPolicy#UPDATE_POLICY_DAILY + * @see RepositoryPolicy#UPDATE_POLICY_NEVER + * @since 2.0.0 + */ + public TestRepositorySystemSession setMetadataUpdatePolicy(String metadataUpdatePolicy) { + verifyStateForMutation(); + this.metadataUpdatePolicy = metadataUpdatePolicy; + return this; + } + + @Override + public LocalRepository getLocalRepository() { + LocalRepositoryManager lrm = getLocalRepositoryManager(); + return (lrm != null) ? lrm.getRepository() : null; + } + + @Override + public LocalRepositoryManager getLocalRepositoryManager() { + return localRepositoryManager; + } + + /** + * Sets the local repository manager used during this session. Note: Eventually, a valid session must have + * a local repository manager set. + * + * @param localRepositoryManager The local repository manager used during this session, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setLocalRepositoryManager(LocalRepositoryManager localRepositoryManager) { + verifyStateForMutation(); + this.localRepositoryManager = localRepositoryManager; + return this; + } + + @Override + public WorkspaceReader getWorkspaceReader() { + return workspaceReader; + } + + /** + * Sets the workspace reader used during this session. If set, the workspace reader will usually be consulted first + * to resolve artifacts. + * + * @param workspaceReader The workspace reader for this session, may be {@code null} if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setWorkspaceReader(WorkspaceReader workspaceReader) { + verifyStateForMutation(); + this.workspaceReader = workspaceReader; + return this; + } + + @Override + public RepositoryListener getRepositoryListener() { + return repositoryListener; + } + + /** + * Sets the listener being notified of actions in the repository system. + * + * @param repositoryListener The repository listener, may be {@code null} if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setRepositoryListener(RepositoryListener repositoryListener) { + verifyStateForMutation(); + this.repositoryListener = repositoryListener; + return this; + } + + @Override + public TransferListener getTransferListener() { + return transferListener; + } + + /** + * Sets the listener being notified of uploads/downloads by the repository system. + * + * @param transferListener The transfer listener, may be {@code null} if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setTransferListener(TransferListener transferListener) { + verifyStateForMutation(); + this.transferListener = transferListener; + return this; + } + + @SuppressWarnings("checkstyle:magicnumber") + private Map copySafe(Map table, Class valueType) { + Map map; + if (table == null || table.isEmpty()) { + map = new HashMap<>(); + } else { + map = new HashMap<>((int) (table.size() / 0.75f) + 1); + for (Map.Entry entry : table.entrySet()) { + Object key = entry.getKey(); + if (key instanceof String) { + Object value = entry.getValue(); + if (valueType.isInstance(value)) { + map.put(key.toString(), valueType.cast(value)); + } + } + } + } + return map; + } + + @Override + public Map getSystemProperties() { + return systemPropertiesView; + } + + /** + * Sets the system properties to use, e.g. for processing of artifact descriptors. System properties are usually + * collected from the runtime environment like {@link System#getProperties()} and environment variables. + *

+ * Note: System properties are of type {@code Map} and any key-value pair in the input map + * that doesn't match this type will be silently ignored. + * + * @param systemProperties The system properties, may be {@code null} or empty if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setSystemProperties(Map systemProperties) { + verifyStateForMutation(); + this.systemProperties = copySafe(systemProperties, String.class); + systemPropertiesView = Collections.unmodifiableMap(this.systemProperties); + return this; + } + + /** + * Sets the specified system property. + * + * @param key The property key, must not be {@code null}. + * @param value The property value, may be {@code null} to remove/unset the property. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setSystemProperty(String key, String value) { + verifyStateForMutation(); + if (value != null) { + systemProperties.put(key, value); + } else { + systemProperties.remove(key); + } + return this; + } + + @Override + public Map getUserProperties() { + return userPropertiesView; + } + + /** + * Sets the user properties to use, e.g. for processing of artifact descriptors. User properties are similar to + * system properties but are set on the discretion of the user and hence are considered of higher priority than + * system properties in case of conflicts. + *

+ * Note: User properties are of type {@code Map} and any key-value pair in the input map + * that doesn't match this type will be silently ignored. + * + * @param userProperties The user properties, may be {@code null} or empty if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setUserProperties(Map userProperties) { + verifyStateForMutation(); + this.userProperties = copySafe(userProperties, String.class); + userPropertiesView = Collections.unmodifiableMap(this.userProperties); + return this; + } + + /** + * Sets the specified user property. + * + * @param key The property key, must not be {@code null}. + * @param value The property value, may be {@code null} to remove/unset the property. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setUserProperty(String key, String value) { + verifyStateForMutation(); + if (value != null) { + userProperties.put(key, value); + } else { + userProperties.remove(key); + } + return this; + } + + @Override + public Map getConfigProperties() { + return configPropertiesView; + } + + /** + * Sets the configuration properties used to tweak internal aspects of the repository system (e.g. thread pooling, + * connector-specific behavior, etc.). + *

+ * Note: Configuration properties are of type {@code Map} and any key-value pair in the + * input map that doesn't match this type will be silently ignored. + * + * @param configProperties The configuration properties, may be {@code null} or empty if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setConfigProperties(Map configProperties) { + verifyStateForMutation(); + this.configProperties = copySafe(configProperties, Object.class); + configPropertiesView = Collections.unmodifiableMap(this.configProperties); + return this; + } + + /** + * Sets the specified configuration property. + * + * @param key The property key, must not be {@code null}. + * @param value The property value, may be {@code null} to remove/unset the property. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setConfigProperty(String key, Object value) { + verifyStateForMutation(); + if (value != null) { + configProperties.put(key, value); + } else { + configProperties.remove(key); + } + return this; + } + + @Override + public MirrorSelector getMirrorSelector() { + return mirrorSelector; + } + + /** + * Sets the mirror selector to use for repositories discovered in artifact descriptors. Note that this selector is + * not used for remote repositories which are passed as request parameters to the repository system, those + * repositories are supposed to denote the effective repositories. + * + * @param mirrorSelector The mirror selector to use, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setMirrorSelector(MirrorSelector mirrorSelector) { + verifyStateForMutation(); + this.mirrorSelector = mirrorSelector; + if (this.mirrorSelector == null) { + this.mirrorSelector = NullMirrorSelector.INSTANCE; + } + return this; + } + + @Override + public ProxySelector getProxySelector() { + return proxySelector; + } + + /** + * Sets the proxy selector to use for repositories discovered in artifact descriptors. Note that this selector is + * not used for remote repositories which are passed as request parameters to the repository system, those + * repositories are supposed to have their proxy (if any) already set. + * + * @param proxySelector The proxy selector to use, may be {@code null}. + * @return This session for chaining, never {@code null}. + * @see RemoteRepository#getProxy() + */ + public TestRepositorySystemSession setProxySelector(ProxySelector proxySelector) { + verifyStateForMutation(); + this.proxySelector = proxySelector; + if (this.proxySelector == null) { + this.proxySelector = NullProxySelector.INSTANCE; + } + return this; + } + + @Override + public AuthenticationSelector getAuthenticationSelector() { + return authenticationSelector; + } + + /** + * Sets the authentication selector to use for repositories discovered in artifact descriptors. Note that this + * selector is not used for remote repositories which are passed as request parameters to the repository system, + * those repositories are supposed to have their authentication (if any) already set. + * + * @param authenticationSelector The authentication selector to use, may be {@code null}. + * @return This session for chaining, never {@code null}. + * @see RemoteRepository#getAuthentication() + */ + public TestRepositorySystemSession setAuthenticationSelector(AuthenticationSelector authenticationSelector) { + verifyStateForMutation(); + this.authenticationSelector = authenticationSelector; + if (this.authenticationSelector == null) { + this.authenticationSelector = NullAuthenticationSelector.INSTANCE; + } + return this; + } + + @Override + public ArtifactTypeRegistry getArtifactTypeRegistry() { + return artifactTypeRegistry; + } + + /** + * Sets the registry of artifact types recognized by this session. + * + * @param artifactTypeRegistry The artifact type registry, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setArtifactTypeRegistry(ArtifactTypeRegistry artifactTypeRegistry) { + verifyStateForMutation(); + this.artifactTypeRegistry = artifactTypeRegistry; + if (this.artifactTypeRegistry == null) { + this.artifactTypeRegistry = NullArtifactTypeRegistry.INSTANCE; + } + return this; + } + + @Override + public DependencyTraverser getDependencyTraverser() { + return dependencyTraverser; + } + + /** + * Sets the dependency traverser to use for building dependency graphs. + * + * @param dependencyTraverser The dependency traverser to use for building dependency graphs, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setDependencyTraverser(DependencyTraverser dependencyTraverser) { + verifyStateForMutation(); + this.dependencyTraverser = dependencyTraverser; + return this; + } + + @Override + public DependencyManager getDependencyManager() { + return dependencyManager; + } + + /** + * Sets the dependency manager to use for building dependency graphs. + * + * @param dependencyManager The dependency manager to use for building dependency graphs, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setDependencyManager(DependencyManager dependencyManager) { + verifyStateForMutation(); + this.dependencyManager = dependencyManager; + return this; + } + + @Override + public DependencySelector getDependencySelector() { + return dependencySelector; + } + + /** + * Sets the dependency selector to use for building dependency graphs. + * + * @param dependencySelector The dependency selector to use for building dependency graphs, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setDependencySelector(DependencySelector dependencySelector) { + verifyStateForMutation(); + this.dependencySelector = dependencySelector; + return this; + } + + @Override + public VersionFilter getVersionFilter() { + return versionFilter; + } + + /** + * Sets the version filter to use for building dependency graphs. + * + * @param versionFilter The version filter to use for building dependency graphs, may be {@code null} to not filter + * versions. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setVersionFilter(VersionFilter versionFilter) { + verifyStateForMutation(); + this.versionFilter = versionFilter; + return this; + } + + @Override + public DependencyGraphTransformer getDependencyGraphTransformer() { + return dependencyGraphTransformer; + } + + /** + * Sets the dependency graph transformer to use for building dependency graphs. + * + * @param dependencyGraphTransformer The dependency graph transformer to use for building dependency graphs, may be + * {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setDependencyGraphTransformer( + DependencyGraphTransformer dependencyGraphTransformer) { + verifyStateForMutation(); + this.dependencyGraphTransformer = dependencyGraphTransformer; + return this; + } + + @Override + public SessionData getData() { + return data; + } + + /** + * Sets the custom data associated with this session. + * + * @param data The session data, may be {@code null}. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setData(SessionData data) { + verifyStateForMutation(); + this.data = data; + if (this.data == null) { + this.data = new DefaultSessionData(); + } + return this; + } + + @Override + public RepositoryCache getCache() { + return cache; + } + + /** + * Sets the cache the repository system may use to save data for future reuse during the session. + * + * @param cache The repository cache, may be {@code null} if none. + * @return This session for chaining, never {@code null}. + */ + public TestRepositorySystemSession setCache(RepositoryCache cache) { + verifyStateForMutation(); + this.cache = cache; + return this; + } + + /** + * Registers onSessionEnded handler, if able to. + * + * @param handler The handler to register + * @return Return {@code true} if registration was possible, otherwise {@code false}. + */ + @Override + public boolean addOnSessionEndedHandler(Runnable handler) { + return onSessionEndedRegistrar.apply(handler); + } + + /** + * Marks this session as read-only such that any future attempts to call its mutators will fail with an exception. + * Marking an already read-only session as read-only has no effect. The session's data and cache remain writable + * though. + */ + public void setReadOnly() { + readOnly = true; + } + + /** + * Verifies this instance state for mutation operations: mutated instance must not be read-only or closed. + */ + private void verifyStateForMutation() { + if (readOnly) { + throw new IllegalStateException("repository system session is read-only"); + } + } + + static class NullProxySelector implements ProxySelector { + + public static final ProxySelector INSTANCE = new NullProxySelector(); + + public Proxy getProxy(RemoteRepository repository) { + requireNonNull(repository, "repository cannot be null"); + return repository.getProxy(); + } + } + + static class NullMirrorSelector implements MirrorSelector { + + public static final MirrorSelector INSTANCE = new NullMirrorSelector(); + + public RemoteRepository getMirror(RemoteRepository repository) { + requireNonNull(repository, "repository cannot be null"); + return null; + } + } + + static class NullAuthenticationSelector implements AuthenticationSelector { + + public static final AuthenticationSelector INSTANCE = new NullAuthenticationSelector(); + + public Authentication getAuthentication(RemoteRepository repository) { + requireNonNull(repository, "repository cannot be null"); + return repository.getAuthentication(); + } + } + + static final class NullArtifactTypeRegistry implements ArtifactTypeRegistry { + + public static final ArtifactTypeRegistry INSTANCE = new NullArtifactTypeRegistry(); + + public ArtifactType get(String typeId) { + return null; + } + } +} diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java index 62c5080e7..a113162e4 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java @@ -181,7 +181,7 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte } this.headers = headers; - this.client = getOrCreateClient(session, repository); + this.client = getOrCreateClient(session, repository, javaVersion); } private URI resolve(TransportTask task) { @@ -223,103 +223,119 @@ protected void implPeek(PeekTask task) throws Exception { @Override protected void implGet(GetTask task) throws Exception { boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; - HttpResponse response; + HttpResponse response = null; - while (true) { - HttpRequest.Builder request = HttpRequest.newBuilder() - .uri(resolve(task)) - .timeout(Duration.ofMillis(requestTimeout)) - .method("GET", HttpRequest.BodyPublishers.noBody()); - headers.forEach(request::setHeader); - - if (resume) { - long resumeOffset = task.getResumeOffset(); - request.header(RANGE, "bytes=" + resumeOffset + '-'); - request.header( - IF_UNMODIFIED_SINCE, - RFC7231.format( - Instant.ofEpochMilli(task.getDataFile().lastModified() - MODIFICATION_THRESHOLD))); - request.header(ACCEPT_ENCODING, "identity"); - } + try { + while (true) { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri(resolve(task)) + .timeout(Duration.ofMillis(requestTimeout)) + .method("GET", HttpRequest.BodyPublishers.noBody()); + headers.forEach(request::setHeader); + + if (resume) { + long resumeOffset = task.getResumeOffset(); + request.header(RANGE, "bytes=" + resumeOffset + '-'); + request.header( + IF_UNMODIFIED_SINCE, + RFC7231.format( + Instant.ofEpochMilli(task.getDataFile().lastModified() - MODIFICATION_THRESHOLD))); + request.header(ACCEPT_ENCODING, "identity"); + } - try { - response = client.send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); - if (response.statusCode() >= MULTIPLE_CHOICES) { - if (resume && response.statusCode() == PRECONDITION_FAILED) { - resume = false; - continue; + try { + response = client.send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + closeBody(response); + if (resume && response.statusCode() == PRECONDITION_FAILED) { + resume = false; + continue; + } + throw new HttpTransporterException(response.statusCode()); } - throw new HttpTransporterException(response.statusCode()); + } catch (ConnectException e) { + closeBody(response); + throw enhance(e); } - } catch (ConnectException e) { - throw enhance(e); + break; } - break; - } - long offset = 0L, - length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L); - if (resume) { - String range = response.headers().firstValue(CONTENT_RANGE).orElse(null); - if (range != null) { - Matcher m = CONTENT_RANGE_PATTERN.matcher(range); - if (!m.matches()) { - throw new IOException("Invalid Content-Range header for partial download: " + range); - } - offset = Long.parseLong(m.group(1)); - length = Long.parseLong(m.group(2)) + 1L; - if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) { - throw new IOException("Invalid Content-Range header for partial download from offset " - + task.getResumeOffset() + ": " + range); + long offset = 0L, + length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L); + if (resume) { + String range = response.headers().firstValue(CONTENT_RANGE).orElse(null); + if (range != null) { + Matcher m = CONTENT_RANGE_PATTERN.matcher(range); + if (!m.matches()) { + throw new IOException("Invalid Content-Range header for partial download: " + range); + } + offset = Long.parseLong(m.group(1)); + length = Long.parseLong(m.group(2)) + 1L; + if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) { + throw new IOException("Invalid Content-Range header for partial download from offset " + + task.getResumeOffset() + ": " + range); + } } } - } - final boolean downloadResumed = offset > 0L; - final File dataFile = task.getDataFile(); - if (dataFile == null) { - try (InputStream is = response.body()) { - utilGet(task, is, true, length, downloadResumed); - } - } else { - try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) { - task.setDataFile(tempFile.getPath().toFile(), downloadResumed); - if (downloadResumed && Files.isRegularFile(dataFile.toPath())) { - try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) { - Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING); - } - } + final boolean downloadResumed = offset > 0L; + final File dataFile = task.getDataFile(); + if (dataFile == null) { try (InputStream is = response.body()) { utilGet(task, is, true, length, downloadResumed); } - tempFile.move(); - } finally { - task.setDataFile(dataFile); + } else { + try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) { + task.setDataFile(tempFile.getPath().toFile(), downloadResumed); + if (downloadResumed && Files.isRegularFile(dataFile.toPath())) { + try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) { + Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + try (InputStream is = response.body()) { + utilGet(task, is, true, length, downloadResumed); + } + tempFile.move(); + } finally { + task.setDataFile(dataFile); + } } - } - if (task.getDataFile() != null) { - String lastModifiedHeader = - response.headers().firstValue(LAST_MODIFIED).orElse(null); // note: Wagon also does first not last - if (lastModifiedHeader != null) { - try { - Files.setLastModifiedTime( - task.getDataFile().toPath(), - FileTime.fromMillis(ZonedDateTime.parse(lastModifiedHeader, RFC7231) - .toInstant() - .toEpochMilli())); - } catch (DateTimeParseException e) { - // fall through + if (task.getDataFile() != null) { + String lastModifiedHeader = response.headers() + .firstValue(LAST_MODIFIED) + .orElse(null); // note: Wagon also does first not last + if (lastModifiedHeader != null) { + try { + Files.setLastModifiedTime( + task.getDataFile().toPath(), + FileTime.fromMillis(ZonedDateTime.parse(lastModifiedHeader, RFC7231) + .toInstant() + .toEpochMilli())); + } catch (DateTimeParseException e) { + // fall through + } } } + Map checksums = extractXChecksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + return; + } + checksums = extractNexus2Checksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + } + } finally { + closeBody(response); } - Map checksums = extractXChecksums(response); - if (checksums != null) { - checksums.forEach(task::setChecksum); - return; - } - checksums = extractNexus2Checksums(response); - if (checksums != null) { - checksums.forEach(task::setChecksum); + } + + private void closeBody(HttpResponse streamHttpResponse) throws IOException { + if (streamHttpResponse != null) { + InputStream body = streamHttpResponse.body(); + if (body != null) { + body.close(); + } } } @@ -416,7 +432,7 @@ private InetAddress getHttpLocalAddress(RepositorySystemSession session, RemoteR */ static final String HTTP_INSTANCE_KEY_PREFIX = JdkTransporterFactory.class.getName() + ".http."; - private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + private HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository, int javaVersion) throws NoTransporterException { final String instanceKey = HTTP_INSTANCE_KEY_PREFIX + repository.getId(); @@ -544,15 +560,7 @@ protected PasswordAuthentication getPasswordAuthentication() { } HttpClient result = builder.build(); - if (!session.addOnSessionEndedHandler(() -> { - if (result instanceof AutoCloseable) { - try { - ((AutoCloseable) client).close(); - } catch (final Exception e) { - throw new IllegalStateException(e); - } - } - })) { + if (!session.addOnSessionEndedHandler(JdkTransporterCloser.closer(javaVersion, result))) { LOGGER.warn( "Using Resolver 2 feature without Resolver 2 session handling, you may leak resources."); } diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java new file mode 100644 index 000000000..cb890efe2 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java @@ -0,0 +1,41 @@ +/* + * 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.eclipse.aether.transport.jdk; + +import java.net.http.HttpClient; + +/** + * JDK Transport that properly closes {@link HttpClient} on Java 11-20. + * + * @since 2.0.0 + */ +final class JdkTransporterCloser { + @SuppressWarnings("checkstyle:MagicNumber") + static Runnable closer(int javaVersion, HttpClient httpClient) { + return () -> { + if (httpClient instanceof AutoCloseable) { + try { + ((AutoCloseable) httpClient).close(); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + }; + } +} diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/pom.xml b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/pom.xml new file mode 100644 index 000000000..ddee7114b --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/pom.xml @@ -0,0 +1,114 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jdk-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jdk-21 + jar + + Maven Artifact Resolver Transport JDK 21 + Maven Artifact Transport JDK Java 11+. + + + org.apache.maven.resolver.transport.jdk + ${Automatic-Module-Name} + + 21 + + + + + org.slf4j + slf4j-api + + + org.apache.maven.resolver + maven-resolver-api + + + org.apache.maven.resolver + maven-resolver-spi + + + org.apache.maven.resolver + maven-resolver-util + + + javax.inject + javax.inject + provided + true + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.slf4j + slf4j-simple + test + + + org.apache.maven.resolver + maven-resolver-test-util + test + + + org.apache.maven.resolver + maven-resolver-test-http + test + + + org.apache.maven.resolver + maven-resolver-impl + test + + + + + + + org.eclipse.sisu + sisu-maven-plugin + + + biz.aQute.bnd + bnd-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java new file mode 100644 index 000000000..1c75c13a7 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-21/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterCloser.java @@ -0,0 +1,32 @@ +/* + * 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.eclipse.aether.transport.jdk; + +import java.net.http.HttpClient; + +/** + * JDK Transport that properly closes {@link HttpClient} on Java 21+. + * + * @since 2.0.0 + */ +final class JdkTransporterCloser { + static Runnable closer(int javaVersion, HttpClient httpClient) { + return httpClient::shutdownNow; + } +} diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml index 521198fea..5d38bb48c 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml @@ -53,6 +53,12 @@ ${project.version} true + + org.apache.maven.resolver + maven-resolver-transport-jdk-21 + ${project.version} + true + org.slf4j @@ -143,6 +149,25 @@ + + java21 + + unpack + + generate-resources + + + + org.apache.maven.resolver + maven-resolver-transport-jdk-21 + ${project.version} + jar + ${project.build.directory}/generated-resources/META-INF/versions/21 + **/*.class + + + + diff --git a/maven-resolver-transport-jdk-parent/pom.xml b/maven-resolver-transport-jdk-parent/pom.xml index 1b4dabcb8..0e547a804 100644 --- a/maven-resolver-transport-jdk-parent/pom.xml +++ b/maven-resolver-transport-jdk-parent/pom.xml @@ -35,6 +35,7 @@ maven-resolver-transport-jdk-8 maven-resolver-transport-jdk-11 + maven-resolver-transport-jdk-21 maven-resolver-transport-jdk diff --git a/pom.xml b/pom.xml index 31b8b505d..d3ba2e6b2 100644 --- a/pom.xml +++ b/pom.xml @@ -105,7 +105,7 @@ 4.0.0-alpha-9 [3.8.8,) - [17,) + [21,) 2023-12-13T22:58:50Z