From 56b97235f2f4be2ed84ef3c032018c342d742a5b Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 20:43:56 +0200 Subject: [PATCH 1/6] [MRESOLVER-416] New transport "jetty" --- .../maven-resolver-demo-snippets/pom.xml | 4 + .../SupplierRepositorySystemFactory.java | 2 + maven-resolver-transport-jdk/pom.xml | 2 +- maven-resolver-transport-jetty/pom.xml | 101 ++++ .../jetty/JettyTransporterFactory.java | 60 +++ .../transport/jetty/JettyException.java | 37 ++ .../transport/jetty/JettyTransporter.java | 450 ++++++++++++++++++ .../jetty/JettyTransporterFactory.java | 64 +++ .../aether/transport/jetty/package-info.java | 26 + pom.xml | 6 + 10 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 maven-resolver-transport-jetty/pom.xml create mode 100644 maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java create mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java create mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java create mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java create mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml index 9f48c0327..e07adfafa 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml +++ b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml @@ -69,6 +69,10 @@ org.apache.maven.resolver maven-resolver-transport-jdk + + org.apache.maven.resolver + maven-resolver-transport-jetty + org.apache.maven.resolver maven-resolver-supplier diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java index 55c02e4d2..56df8a19a 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java @@ -25,6 +25,7 @@ import org.eclipse.aether.supplier.RepositorySystemSupplier; import org.eclipse.aether.transport.http.ChecksumExtractor; import org.eclipse.aether.transport.jdk.JdkTransporterFactory; +import org.eclipse.aether.transport.jetty.JettyTransporterFactory; /** * A factory for repository system instances that employs Maven Artifact Resolver's provided supplier. @@ -37,6 +38,7 @@ protected Map getTransporterFactories( Map extractors) { Map result = super.getTransporterFactories(extractors); result.put(JdkTransporterFactory.NAME, new JdkTransporterFactory()); + result.put(JettyTransporterFactory.NAME, new JettyTransporterFactory()); return result; } }.get(); diff --git a/maven-resolver-transport-jdk/pom.xml b/maven-resolver-transport-jdk/pom.xml index c0705e1ae..eed155596 100644 --- a/maven-resolver-transport-jdk/pom.xml +++ b/maven-resolver-transport-jdk/pom.xml @@ -72,7 +72,7 @@ 11 true - src/main/java11 + ${project.basedir}/src/main/java11 diff --git a/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty/pom.xml new file mode 100644 index 000000000..b239bcaa1 --- /dev/null +++ b/maven-resolver-transport-jetty/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jetty + jar + + Maven Artifact Resolver Transport Jetty + Maven Artifact Transport Jetty Java 11+. + + + org.apache.maven.resolver.transport.jetty + ${Automatic-Module-Name} + + 10.0.17 + + + + + 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.eclipse.jetty + jetty-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jettyVersion} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + java11 + + compile + + + 11 + true + ${project.basedir}/src/main/java11 + + + + + + + + diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java new file mode 100644 index 000000000..38f140768 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -0,0 +1,60 @@ +/* + * 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.jetty; + +import javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * Jetty Transport factory: on Java 8 is no-op. + * + * @since TBD + */ +@Named(JettyTransporterFactory.NAME) +public final class JettyTransporterFactory implements TransporterFactory { + public static final String NAME = "jetty"; + + private float priority = 15.0f; + + @Override + public float getPriority() { + return priority; + } + + public JettyTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + throw new NoTransporterException(repository, "Jetty Transport needs Java11+"); + } +} diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java new file mode 100644 index 000000000..3cd907bf2 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java @@ -0,0 +1,37 @@ +/* + * 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.jetty; + +/** + * Exception thrown by {@link JettyTransporter} in case of errors. + * + * @since TBD + */ +final class JettyException extends Exception { + private final int statusCode; + + JettyException(int statusCode) { + super("HTTP Status: " + statusCode); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java new file mode 100644 index 000000000..69bb1c060 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -0,0 +1,450 @@ +/* + * 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.jetty; + +import javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.PeekTask; +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.eclipse.jetty.client.util.InputStreamResponseListener; +import org.eclipse.jetty.client.util.PathRequestContent; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +/** + * A transporter for HTTP/HTTPS. + * + * @since TBD + */ +final class JettyTransporter extends AbstractTransporter { + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); + + private final URI baseUri; + + private final HttpClient client; + + private final int requestTimeout; + + private final Map headers; + + JettyTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { + try { + URI uri = new URI(repository.getUrl()).parseServerAuthority(); + if (uri.isOpaque()) { + throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); + } + if (uri.getRawFragment() != null || uri.getRawQuery() != null) { + throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); + } + String path = uri.getPath(); + if (path == null) { + path = "/"; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/")) { + path = path + "/"; + } + this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( + session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT); + if (userAgent != null) { + headers.put(USER_AGENT, userAgent); + } + @SuppressWarnings("unchecked") + Map configuredHeaders = (Map) ConfigUtils.getMap( + session, + Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS); + if (configuredHeaders != null) { + configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null)); + } + + this.headers = headers; + + this.requestTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT); + + this.client = getOrCreateClient(session, repository); + } + + private URI resolve(TransportTask task) { + return baseUri.resolve(task.getLocation()); + } + + @Override + public int classify(Throwable error) { + if (error instanceof JettyException && ((JettyException) error).getStatusCode() == NOT_FOUND) { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + Request request = client.newRequest(resolve(task)) + .timeout(requestTimeout, TimeUnit.MILLISECONDS) + .method("HEAD"); + request.headers(m -> headers.forEach(m::add)); + Response response = request.send(); + if (response.getStatus() >= MULTIPLE_CHOICES) { + throw new JettyException(response.getStatus()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + Response response; + InputStreamResponseListener listener; + + while (true) { + Request request = client.newRequest(resolve(task)) + .timeout(requestTimeout, TimeUnit.MILLISECONDS) + .method("GET"); + request.headers(m -> headers.forEach(m::add)); + + if (resume) { + long resumeOffset = task.getResumeOffset(); + request.headers(h -> { + h.add(RANGE, "bytes=" + resumeOffset + '-'); + h.addDateField(IF_UNMODIFIED_SINCE, task.getDataFile().lastModified() - MODIFICATION_THRESHOLD); + h.remove(HttpHeader.ACCEPT_ENCODING); + h.add(ACCEPT_ENCODING, "identity"); + }); + } + + listener = new InputStreamResponseListener(); + request.send(listener); + try { + response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else { + throw new RuntimeException(t); + } + } + if (response.getStatus() >= MULTIPLE_CHOICES) { + if (resume && response.getStatus() == PRECONDITION_FAILED) { + resume = false; + continue; + } + throw new JettyException(response.getStatus()); + } + break; + } + + long offset = 0L, length = response.getHeaders().getLongField(CONTENT_LENGTH); + if (resume) { + String range = response.getHeaders().get(CONTENT_RANGE); + 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 = listener.getInputStream()) { + 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); + } + } + try (InputStream is = listener.getInputStream()) { + utilGet(task, is, true, length, downloadResumed); + } + tempFile.move(); + } finally { + task.setDataFile(dataFile); + } + } + Map checksums = extractXChecksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + return; + } + checksums = extractNexus2Checksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + } + } + + private static Map extractXChecksums(Response response) { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get("x-checksum-sha1"); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get("x-checksum-md5"); + if (value != null) { + result.put("MD5", value); + } + if (!result.isEmpty()) { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get("x-goog-meta-checksum-sha1"); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get("x-goog-meta-checksum-md5"); + if (value != null) { + result.put("MD5", value); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums(Response response) { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.getHeaders().get("ETag"); + if (etag != null) { + int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5); + if (start >= 0 && end > start) { + return Collections.singletonMap("SHA-1", etag.substring(start + 5, end)); + } + } + return null; + } + + @Override + protected void implPut(PutTask task) throws Exception { + Request request = client.newRequest(resolve(task)).method("PUT").timeout(requestTimeout, TimeUnit.MILLISECONDS); + request.headers(m -> headers.forEach(m::add)); + try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { + utilPut(task, Files.newOutputStream(tempFile.getPath()), true); + request.body(new PathRequestContent(tempFile.getPath())); + + Response response; + try { + response = request.send(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else { + throw new RuntimeException(t); + } + } + if (response.getStatus() >= MULTIPLE_CHOICES) { + throw new JettyException(response.getStatus()); + } + } + } + + @Override + protected void implClose() { + // noop + } + + /** + * Visible for testing. + */ + static final String JETTY_INSTANCE_KEY_PREFIX = JettyTransporterFactory.class.getName() + ".jetty."; + + @SuppressWarnings("checkstyle:methodlength") + private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + + final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); + + try { + return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { + SSLContext sslContext = null; + BasicAuthentication basicAuthentication = null; + try { + try (AuthenticationContext repoAuthContext = + AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class); + + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + + basicAuthentication = new BasicAuthentication( + URI.create(repository.getUrl()), Authentication.ANY_REALM, username, password); + } + } + + if (sslContext == null) { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setSslContext(sslContext); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + HttpClientTransportDynamic transport; + if ("https".equalsIgnoreCase(repository.getProtocol())) { + transport = new HttpClientTransportDynamic( + clientConnector, http2, HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2 + } else { + transport = new HttpClientTransportDynamic( + clientConnector, + HttpClientConnectionFactory.HTTP11, + http2); // plaintext HTTP, H2 cannot be used + } + + HttpClient httpClient = new HttpClient(transport); + httpClient.setConnectTimeout(connectTimeout); + httpClient.setFollowRedirects(true); + httpClient.setMaxRedirects(2); + + httpClient.setUserAgentField(null); // we manage it + + if (basicAuthentication != null) { + httpClient.getAuthenticationStore().addAuthentication(basicAuthentication); + } + + if (repository.getProxy() != null) { + HttpProxy proxy = new HttpProxy( + repository.getProxy().getHost(), + repository.getProxy().getPort()); + + httpClient.getProxyConfiguration().addProxy(proxy); + try (AuthenticationContext proxyAuthContext = + AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + + BasicAuthentication proxyAuthentication = new BasicAuthentication( + proxy.getURI(), Authentication.ANY_REALM, username, password); + + httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication); + } + } + } + httpClient.start(); + return httpClient; + } catch (Exception e) { + throw new WrapperEx(e); + } + }); + } catch (WrapperEx e) { + throw new NoTransporterException(repository, e.getCause()); + } + } + + private static final class WrapperEx extends RuntimeException { + private WrapperEx(Throwable cause) { + super(cause); + } + } +} \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java new file mode 100644 index 000000000..ac5166802 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -0,0 +1,64 @@ +/* + * 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.jetty; + +import javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * A transporter factory for repositories using the {@code http:} or {@code https:} protocol. + * + * @since TBD + */ +@Named(JettyTransportFactory.NAME) +public final class JettyTransporterFactory implements TransporterFactory { + public static final String NAME = "jetty"; + + private float priority = 15.0f; + + @Override + public float getPriority() { + return priority; + } + + public JettyTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) { + throw new NoTransporterException(repository); + } + + return new JettyTransporter(session, repository); + } +} \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java new file mode 100644 index 000000000..367c20573 --- /dev/null +++ b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java @@ -0,0 +1,26 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The implementation is backed by + * Eclipse Jetty. + * + * @since TBD + */ +package org.eclipse.aether.transport.jetty; diff --git a/pom.xml b/pom.xml index c895375a9..d7cb1c158 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ maven-resolver-connector-basic maven-resolver-transport-classpath maven-resolver-transport-file + maven-resolver-transport-jetty maven-resolver-transport-jdk maven-resolver-transport-http maven-resolver-transport-wagon @@ -143,6 +144,11 @@ maven-resolver-transport-file ${project.version} + + org.apache.maven.resolver + maven-resolver-transport-jetty + ${project.version} + org.apache.maven.resolver maven-resolver-transport-jdk From 2a14d9e58c82b55d75f8ab8e41ed906dfe66c1f2 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 21:41:36 +0200 Subject: [PATCH 2/6] WIP --- .../maven-resolver-transport-jdk-11/pom.xml | 85 ++++ .../transport/jdk/JdkHttpException.java | 0 .../transport/jdk/JdkHttpTransporter.java | 0 .../transport/jdk/JdkTransporterFactory.java | 64 +++ .../aether/transport/jdk/package-info.java | 0 .../maven-resolver-transport-jdk-8}/pom.xml | 34 +- .../transport/jdk/JdkTransporterFactory.java | 0 .../maven-resolver-transport-jdk/pom.xml | 128 ++++++ maven-resolver-transport-jdk-parent/pom.xml | 41 ++ .../transport/jdk/JdkTransporterFactory.java | 60 +++ .../transport/jdk/JdkHttpException.java | 37 ++ .../transport/jdk/JdkHttpTransporter.java | 435 ++++++++++++++++++ .../transport/jdk/JdkTransporterFactory.java | 0 .../aether/transport/jdk/package-info.java | 26 ++ pom.xml | 2 +- 15 files changed, 894 insertions(+), 18 deletions(-) create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml rename {maven-resolver-transport-jdk/src/main/java11 => maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java}/org/eclipse/aether/transport/jdk/JdkHttpException.java (100%) rename {maven-resolver-transport-jdk/src/main/java11 => maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java}/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java (100%) create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java rename {maven-resolver-transport-jdk/src/main/java11 => maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java}/org/eclipse/aether/transport/jdk/package-info.java (100%) rename {maven-resolver-transport-jdk => maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8}/pom.xml (77%) rename {maven-resolver-transport-jdk => maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8}/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java (100%) create mode 100644 maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml create mode 100644 maven-resolver-transport-jdk-parent/pom.xml create mode 100644 maven-resolver-transport-jdk-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java create mode 100644 maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java create mode 100644 maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java rename {maven-resolver-transport-jdk => maven-resolver-transport-jdk-parent}/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java (100%) create mode 100644 maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml new file mode 100644 index 000000000..b8f544d8c --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jdk-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jdk-11 + jar + + Maven Artifact Resolver Transport JDK (11) + Maven Artifact Transport JDK Java 11+. + + + org.apache.maven.resolver.transport.jdk + ${Automatic-Module-Name} + + 11 + + + + + 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.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/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpException.java similarity index 100% rename from maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java rename to maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpException.java diff --git a/maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java similarity index 100% rename from maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java rename to maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java new file mode 100644 index 000000000..ef076f038 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -0,0 +1,64 @@ +/* + * 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 javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * JDK Transport factory. + * + * @since TBD + */ +@Named(JdkTransporterFactory.NAME) +public final class JdkTransporterFactory implements TransporterFactory { + public static final String NAME = "jdk"; + + private float priority = 10.0f; + + @Override + public float getPriority() { + return priority; + } + + public JdkTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) { + throw new NoTransporterException(repository, "Only HTTP/HTTPS is supported"); + } + + return new JdkHttpTransporter(session, repository); + } +} diff --git a/maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/package-info.java similarity index 100% rename from maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java rename to maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/package-info.java diff --git a/maven-resolver-transport-jdk/pom.xml b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/pom.xml similarity index 77% rename from maven-resolver-transport-jdk/pom.xml rename to maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/pom.xml index eed155596..ff0847481 100644 --- a/maven-resolver-transport-jdk/pom.xml +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/pom.xml @@ -22,14 +22,14 @@ org.apache.maven.resolver - maven-resolver + maven-resolver-transport-jdk-parent 2.0.0-SNAPSHOT - maven-resolver-transport-jdk + maven-resolver-transport-jdk-8 jar - Maven Artifact Resolver Transport JDK + Maven Artifact Resolver Transport JDK (8) Maven Artifact Transport JDK Java 11+. @@ -60,22 +60,22 @@ + + org.eclipse.sisu + sisu-maven-plugin + + + biz.aQute.bnd + bnd-maven-plugin + org.apache.maven.plugins - maven-compiler-plugin - - - java11 - - compile - - - 11 - true - ${project.basedir}/src/main/java11 - - - + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + diff --git a/maven-resolver-transport-jdk/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java similarity index 100% rename from maven-resolver-transport-jdk/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java rename to maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java 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 new file mode 100644 index 000000000..f6b32f9a7 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jdk-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jdk + jar + + Maven Artifact Resolver Transport JDK (mr) + Maven Artifact Transport JDK Java 11+. + + + org.apache.maven.resolver.transport.jdk + ${Automatic-Module-Name} + + + + + 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 + + + + + + + false + ${project.build.directory}/generated-resources + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + java8 + + unpack + + generate-resources + + + + org.apache.maven.resolver + maven-resolver-transport-jdk-8 + ${project.version} + jar + ${project.build.directory}/generated-resources + META-INF/maven/** + + + + + + java11 + + unpack + + generate-resources + + + + org.apache.maven.resolver + maven-resolver-transport-jdk-11 + ${project.version} + jar + ${project.build.directory}/generated-resources/META-INF/versions/11 + **/*.class + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + true + + + + + + + + diff --git a/maven-resolver-transport-jdk-parent/pom.xml b/maven-resolver-transport-jdk-parent/pom.xml new file mode 100644 index 000000000..d25f5805d --- /dev/null +++ b/maven-resolver-transport-jdk-parent/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jdk-parent + pom + + Maven Artifact Resolver Transport JDK (parent) + Maven Artifact Transport JDK Java 11+. + + + maven-resolver-transport-jdk-8 + maven-resolver-transport-jdk-11 + maven-resolver-transport-jdk + + + diff --git a/maven-resolver-transport-jdk-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java new file mode 100644 index 000000000..aa015d2b2 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -0,0 +1,60 @@ +/* + * 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 javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * JDK Transport factory: on Java 8 is no-op. + * + * @since TBD + */ +@Named(JdkTransporterFactory.NAME) +public final class JdkTransporterFactory implements TransporterFactory { + public static final String NAME = "jdk"; + + private float priority = 10.0f; + + @Override + public float getPriority() { + return priority; + } + + public JdkTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + throw new NoTransporterException(repository, "JDK Transport needs Java11+"); + } +} diff --git a/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java new file mode 100644 index 000000000..6aaaead40 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Exception thrown by {@link JdkHttpTransporter} in case of errors. + * + * @since TBD + */ +final class JdkHttpException extends Exception { + private final int statusCode; + + JdkHttpException(int statusCode) { + super("HTTP Status: " + statusCode); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java new file mode 100644 index 000000000..2200aa870 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java @@ -0,0 +1,435 @@ +/* + * 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 javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.PeekTask; +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; + +/** + * JDK Transport using {@link HttpClient}. + * + * @since TBD + */ +final class JdkHttpTransporter extends AbstractTransporter { + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CACHE_CONTROL = "Cache-Control"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final String LAST_MODIFIED = "Last-Modified"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); + + private final URI baseUri; + + private final HttpClient client; + + private final Map headers; + + private final int requestTimeout; + + JdkHttpTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { + try { + URI uri = new URI(repository.getUrl()).parseServerAuthority(); + if (uri.isOpaque()) { + throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); + } + if (uri.getRawFragment() != null || uri.getRawQuery() != null) { + throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); + } + String path = uri.getPath(); + if (path == null) { + path = "/"; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/")) { + path = path + "/"; + } + this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( + session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT); + if (userAgent != null) { + headers.put(USER_AGENT, userAgent); + } + @SuppressWarnings("unchecked") + Map configuredHeaders = (Map) ConfigUtils.getMap( + session, + Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS); + if (configuredHeaders != null) { + configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null)); + } + headers.put(CACHE_CONTROL, "no-cache, no-store"); + + this.requestTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT); + + this.headers = headers; + this.client = getOrCreateClient(session, repository); + } + + private URI resolve(TransportTask task) { + return baseUri.resolve(task.getLocation()); + } + + @Override + public int classify(Throwable error) { + if (error instanceof JdkHttpException && ((JdkHttpException) error).getStatusCode() == NOT_FOUND) { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri(resolve(task)) + .timeout(Duration.ofMillis(requestTimeout)) + .method("HEAD", HttpRequest.BodyPublishers.noBody()); + headers.forEach(request::setHeader); + HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + throw new JdkHttpException(response.statusCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + HttpResponse response; + + 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"); + } + + response = client.send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + if (resume && response.statusCode() == PRECONDITION_FAILED) { + resume = false; + continue; + } + throw new JdkHttpException(response.statusCode()); + } + 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); + } + } + } + + 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); + } + } + 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 + } + } + } + Map checksums = extractXChecksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + return; + } + checksums = extractNexus2Checksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + } + } + + @Override + protected void implPut(PutTask task) throws Exception { + HttpRequest.Builder request = + HttpRequest.newBuilder().uri(resolve(task)).timeout(Duration.ofMillis(requestTimeout)); + headers.forEach(request::setHeader); + try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { + utilPut(task, Files.newOutputStream(tempFile.getPath()), true); + request.method("PUT", HttpRequest.BodyPublishers.ofFile(tempFile.getPath())); + + HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + throw new JdkHttpException(response.statusCode()); + } + } + } + + @Override + protected void implClose() { + // nop + } + + private static Map extractXChecksums(HttpResponse response) { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue("x-checksum-sha1").orElse(null); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue("x-checksum-md5").orElse(null); + if (value != null) { + result.put("MD5", value); + } + if (!result.isEmpty()) { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue("x-goog-meta-checksum-sha1").orElse(null); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue("x-goog-meta-checksum-md5").orElse(null); + if (value != null) { + result.put("MD5", value); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums(HttpResponse response) { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.headers().firstValue("ETag").orElse(null); + if (etag != null) { + int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5); + if (start >= 0 && end > start) { + return Collections.singletonMap("SHA-1", etag.substring(start + 5, end)); + } + } + return null; + } + + private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern( + "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + .withZone(ZoneId.of("GMT")); + + /** + * Visible for testing. + */ + static final String HTTP_INSTANCE_KEY_PREFIX = JdkTransporterFactory.class.getName() + ".http."; + + private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + final String instanceKey = HTTP_INSTANCE_KEY_PREFIX + repository.getId(); + + try { + return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { + HashMap authentications = new HashMap<>(); + SSLContext sslContext = null; + try { + try (AuthenticationContext repoAuthContext = + AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class); + + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + + authentications.put( + Authenticator.RequestorType.SERVER, + new PasswordAuthentication(username, password.toCharArray())); + } + } + + if (sslContext == null) { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT); + + HttpClient.Builder builder = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofMillis(connectTimeout)) + .sslContext(sslContext); + + if (repository.getProxy() != null) { + ProxySelector proxy = ProxySelector.of(new InetSocketAddress( + repository.getProxy().getHost(), + repository.getProxy().getPort())); + + builder.proxy(proxy); + try (AuthenticationContext proxyAuthContext = + AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + + authentications.put( + Authenticator.RequestorType.PROXY, + new PasswordAuthentication(username, password.toCharArray())); + } + } + } + + if (!authentications.isEmpty()) { + builder.authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return authentications.get(getRequestorType()); + } + }); + } + + return builder.build(); + } catch (NoSuchAlgorithmException e) { + throw new WrapperEx(e); + } + }); + } catch (WrapperEx e) { + throw new NoTransporterException(repository, e.getCause()); + } + } + + private static final class WrapperEx extends RuntimeException { + private WrapperEx(Throwable cause) { + super(cause); + } + } +} diff --git a/maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java similarity index 100% rename from maven-resolver-transport-jdk/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java rename to maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java diff --git a/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java new file mode 100644 index 000000000..8ef1b81a9 --- /dev/null +++ b/maven-resolver-transport-jdk-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java @@ -0,0 +1,26 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The implementation is backed by + * Java 11 {@link java.net.http.HttpClient}. + * + * @since TBD + */ +package org.eclipse.aether.transport.jdk; diff --git a/pom.xml b/pom.xml index d7cb1c158..43d9d3d7f 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ maven-resolver-transport-classpath maven-resolver-transport-file maven-resolver-transport-jetty - maven-resolver-transport-jdk + maven-resolver-transport-jdk-parent maven-resolver-transport-http maven-resolver-transport-wagon maven-resolver-supplier From ab3035d0eb5e8e8dddc7d03c94e1a28791d352ce Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 21:54:36 +0200 Subject: [PATCH 3/6] WIP --- .../maven-resolver-transport-jetty-11/pom.xml | 102 ++++ .../transport/jetty/JettyException.java | 37 ++ .../transport/jetty/JettyTransporter.java | 450 ++++++++++++++++++ .../jetty/JettyTransporterFactory.java | 64 +++ .../aether/transport/jetty/package-info.java | 26 + .../maven-resolver-transport-jetty-8/pom.xml | 83 ++++ .../jetty/JettyTransporterFactory.java | 60 +++ .../maven-resolver-transport-jetty/pom.xml | 128 +++++ maven-resolver-transport-jetty-parent/pom.xml | 41 ++ .../transport/jdk/JdkTransporterFactory.java | 60 +++ .../transport/jdk/JdkHttpException.java | 37 ++ .../transport/jdk/JdkHttpTransporter.java | 435 +++++++++++++++++ .../transport/jdk/JdkTransporterFactory.java | 64 +++ .../aether/transport/jdk/package-info.java | 26 + pom.xml | 2 +- 15 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/package-info.java create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/pom.xml create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java create mode 100644 maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml create mode 100644 maven-resolver-transport-jetty-parent/pom.xml create mode 100644 maven-resolver-transport-jetty-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java create mode 100644 maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java create mode 100644 maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java create mode 100644 maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java create mode 100644 maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml new file mode 100644 index 000000000..760bc529d --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jetty-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jetty-11 + jar + + Maven Artifact Resolver Transport Jetty (11) + Maven Artifact Transport Jetty. + + + org.apache.maven.resolver.transport.jetty + ${Automatic-Module-Name} + + 11 + 10.0.17 + + + + + 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.eclipse.jetty + jetty-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jettyVersion} + + + + + + + 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-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java new file mode 100644 index 000000000..3cd907bf2 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyException.java @@ -0,0 +1,37 @@ +/* + * 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.jetty; + +/** + * Exception thrown by {@link JettyTransporter} in case of errors. + * + * @since TBD + */ +final class JettyException extends Exception { + private final int statusCode; + + JettyException(int statusCode) { + super("HTTP Status: " + statusCode); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java new file mode 100644 index 000000000..56f8d93eb --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java @@ -0,0 +1,450 @@ +/* + * 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.jetty; + +import javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.PeekTask; +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.client.util.BasicAuthentication; +import org.eclipse.jetty.client.util.InputStreamResponseListener; +import org.eclipse.jetty.client.util.PathRequestContent; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +/** + * A transporter for HTTP/HTTPS. + * + * @since TBD + */ +final class JettyTransporter extends AbstractTransporter { + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); + + private final URI baseUri; + + private final HttpClient client; + + private final int requestTimeout; + + private final Map headers; + + JettyTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { + try { + URI uri = new URI(repository.getUrl()).parseServerAuthority(); + if (uri.isOpaque()) { + throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); + } + if (uri.getRawFragment() != null || uri.getRawQuery() != null) { + throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); + } + String path = uri.getPath(); + if (path == null) { + path = "/"; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/")) { + path = path + "/"; + } + this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( + session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT); + if (userAgent != null) { + headers.put(USER_AGENT, userAgent); + } + @SuppressWarnings("unchecked") + Map configuredHeaders = (Map) ConfigUtils.getMap( + session, + Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS); + if (configuredHeaders != null) { + configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null)); + } + + this.headers = headers; + + this.requestTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT); + + this.client = getOrCreateClient(session, repository); + } + + private URI resolve(TransportTask task) { + return baseUri.resolve(task.getLocation()); + } + + @Override + public int classify(Throwable error) { + if (error instanceof JettyException && ((JettyException) error).getStatusCode() == NOT_FOUND) { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + Request request = client.newRequest(resolve(task)) + .timeout(requestTimeout, TimeUnit.MILLISECONDS) + .method("HEAD"); + request.headers(m -> headers.forEach(m::add)); + Response response = request.send(); + if (response.getStatus() >= MULTIPLE_CHOICES) { + throw new JettyException(response.getStatus()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + Response response; + InputStreamResponseListener listener; + + while (true) { + Request request = client.newRequest(resolve(task)) + .timeout(requestTimeout, TimeUnit.MILLISECONDS) + .method("GET"); + request.headers(m -> headers.forEach(m::add)); + + if (resume) { + long resumeOffset = task.getResumeOffset(); + request.headers(h -> { + h.add(RANGE, "bytes=" + resumeOffset + '-'); + h.addDateField(IF_UNMODIFIED_SINCE, task.getDataFile().lastModified() - MODIFICATION_THRESHOLD); + h.remove(HttpHeader.ACCEPT_ENCODING); + h.add(ACCEPT_ENCODING, "identity"); + }); + } + + listener = new InputStreamResponseListener(); + request.send(listener); + try { + response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else { + throw new RuntimeException(t); + } + } + if (response.getStatus() >= MULTIPLE_CHOICES) { + if (resume && response.getStatus() == PRECONDITION_FAILED) { + resume = false; + continue; + } + throw new JettyException(response.getStatus()); + } + break; + } + + long offset = 0L, length = response.getHeaders().getLongField(CONTENT_LENGTH); + if (resume) { + String range = response.getHeaders().get(CONTENT_RANGE); + 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 = listener.getInputStream()) { + 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); + } + } + try (InputStream is = listener.getInputStream()) { + utilGet(task, is, true, length, downloadResumed); + } + tempFile.move(); + } finally { + task.setDataFile(dataFile); + } + } + Map checksums = extractXChecksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + return; + } + checksums = extractNexus2Checksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + } + } + + private static Map extractXChecksums(Response response) { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get("x-checksum-sha1"); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get("x-checksum-md5"); + if (value != null) { + result.put("MD5", value); + } + if (!result.isEmpty()) { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.getHeaders().get("x-goog-meta-checksum-sha1"); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.getHeaders().get("x-goog-meta-checksum-md5"); + if (value != null) { + result.put("MD5", value); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums(Response response) { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.getHeaders().get("ETag"); + if (etag != null) { + int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5); + if (start >= 0 && end > start) { + return Collections.singletonMap("SHA-1", etag.substring(start + 5, end)); + } + } + return null; + } + + @Override + protected void implPut(PutTask task) throws Exception { + Request request = client.newRequest(resolve(task)).method("PUT").timeout(requestTimeout, TimeUnit.MILLISECONDS); + request.headers(m -> headers.forEach(m::add)); + try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { + utilPut(task, Files.newOutputStream(tempFile.getPath()), true); + request.body(new PathRequestContent(tempFile.getPath())); + + Response response; + try { + response = request.send(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else { + throw new RuntimeException(t); + } + } + if (response.getStatus() >= MULTIPLE_CHOICES) { + throw new JettyException(response.getStatus()); + } + } + } + + @Override + protected void implClose() { + // noop + } + + /** + * Visible for testing. + */ + static final String JETTY_INSTANCE_KEY_PREFIX = JettyTransporterFactory.class.getName() + ".jetty."; + + @SuppressWarnings("checkstyle:methodlength") + private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + + final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); + + try { + return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { + SSLContext sslContext = null; + BasicAuthentication basicAuthentication = null; + try { + try (AuthenticationContext repoAuthContext = + AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class); + + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + + basicAuthentication = new BasicAuthentication( + URI.create(repository.getUrl()), Authentication.ANY_REALM, username, password); + } + } + + if (sslContext == null) { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setSslContext(sslContext); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + HttpClientTransportDynamic transport; + if ("https".equalsIgnoreCase(repository.getProtocol())) { + transport = new HttpClientTransportDynamic( + clientConnector, http2, HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2 + } else { + transport = new HttpClientTransportDynamic( + clientConnector, + HttpClientConnectionFactory.HTTP11, + http2); // plaintext HTTP, H2 cannot be used + } + + HttpClient httpClient = new HttpClient(transport); + httpClient.setConnectTimeout(connectTimeout); + httpClient.setFollowRedirects(true); + httpClient.setMaxRedirects(2); + + httpClient.setUserAgentField(null); // we manage it + + if (basicAuthentication != null) { + httpClient.getAuthenticationStore().addAuthentication(basicAuthentication); + } + + if (repository.getProxy() != null) { + HttpProxy proxy = new HttpProxy( + repository.getProxy().getHost(), + repository.getProxy().getPort()); + + httpClient.getProxyConfiguration().addProxy(proxy); + try (AuthenticationContext proxyAuthContext = + AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + + BasicAuthentication proxyAuthentication = new BasicAuthentication( + proxy.getURI(), Authentication.ANY_REALM, username, password); + + httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication); + } + } + } + httpClient.start(); + return httpClient; + } catch (Exception e) { + throw new WrapperEx(e); + } + }); + } catch (WrapperEx e) { + throw new NoTransporterException(repository, e.getCause()); + } + } + + private static final class WrapperEx extends RuntimeException { + private WrapperEx(Throwable cause) { + super(cause); + } + } +} diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java new file mode 100644 index 000000000..9d50d60d4 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -0,0 +1,64 @@ +/* + * 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.jetty; + +import javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * A transporter factory for repositories using the {@code http:} or {@code https:} protocol. + * + * @since TBD + */ +@Named(JettyTransporterFactory.NAME) +public final class JettyTransporterFactory implements TransporterFactory { + public static final String NAME = "jetty"; + + private float priority = 15.0f; + + @Override + public float getPriority() { + return priority; + } + + public JettyTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) { + throw new NoTransporterException(repository); + } + + return new JettyTransporter(session, repository); + } +} diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/package-info.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/package-info.java new file mode 100644 index 000000000..367c20573 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/src/main/java/org/eclipse/aether/transport/jetty/package-info.java @@ -0,0 +1,26 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The implementation is backed by + * Eclipse Jetty. + * + * @since TBD + */ +package org.eclipse.aether.transport.jetty; diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/pom.xml b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/pom.xml new file mode 100644 index 000000000..9620f97ad --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jetty-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jetty-8 + jar + + Maven Artifact Resolver Transport Jetty (8) + Maven Artifact Transport Jetty. + + + org.apache.maven.resolver.transport.jetty + ${Automatic-Module-Name} + + + + + 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.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-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java new file mode 100644 index 000000000..38f140768 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -0,0 +1,60 @@ +/* + * 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.jetty; + +import javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * Jetty Transport factory: on Java 8 is no-op. + * + * @since TBD + */ +@Named(JettyTransporterFactory.NAME) +public final class JettyTransporterFactory implements TransporterFactory { + public static final String NAME = "jetty"; + + private float priority = 15.0f; + + @Override + public float getPriority() { + return priority; + } + + public JettyTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + throw new NoTransporterException(repository, "Jetty Transport needs Java11+"); + } +} diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml new file mode 100644 index 000000000..c791b3ef7 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver-transport-jetty-parent + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jetty + jar + + Maven Artifact Resolver Transport Jetty (mr) + Maven Artifact Transport Jetty. + + + org.apache.maven.resolver.transport.jetty + ${Automatic-Module-Name} + + + + + 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 + + + + + + + false + ${project.build.directory}/generated-resources + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + java8 + + unpack + + generate-resources + + + + org.apache.maven.resolver + maven-resolver-transport-jetty-8 + ${project.version} + jar + ${project.build.directory}/generated-resources + META-INF/maven/** + + + + + + java11 + + unpack + + generate-resources + + + + org.apache.maven.resolver + maven-resolver-transport-jetty-11 + ${project.version} + jar + ${project.build.directory}/generated-resources/META-INF/versions/11 + **/*.class + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + true + + + + + + + + diff --git a/maven-resolver-transport-jetty-parent/pom.xml b/maven-resolver-transport-jetty-parent/pom.xml new file mode 100644 index 000000000..5d600f36a --- /dev/null +++ b/maven-resolver-transport-jetty-parent/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + + + org.apache.maven.resolver + maven-resolver + 2.0.0-SNAPSHOT + + + maven-resolver-transport-jetty-parent + pom + + Maven Artifact Resolver Transport Jetty (parent) + Maven Artifact Transport Jetty. + + + maven-resolver-transport-jetty-8 + maven-resolver-transport-jetty-11 + maven-resolver-transport-jetty + + + diff --git a/maven-resolver-transport-jetty-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jetty-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java new file mode 100644 index 000000000..aa015d2b2 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -0,0 +1,60 @@ +/* + * 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 javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * JDK Transport factory: on Java 8 is no-op. + * + * @since TBD + */ +@Named(JdkTransporterFactory.NAME) +public final class JdkTransporterFactory implements TransporterFactory { + public static final String NAME = "jdk"; + + private float priority = 10.0f; + + @Override + public float getPriority() { + return priority; + } + + public JdkTransporterFactory setPriority(float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + throw new NoTransporterException(repository, "JDK Transport needs Java11+"); + } +} diff --git a/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java new file mode 100644 index 000000000..6aaaead40 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpException.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Exception thrown by {@link JdkHttpTransporter} in case of errors. + * + * @since TBD + */ +final class JdkHttpException extends Exception { + private final int statusCode; + + JdkHttpException(int statusCode) { + super("HTTP Status: " + statusCode); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java new file mode 100644 index 000000000..2200aa870 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkHttpTransporter.java @@ -0,0 +1,435 @@ +/* + * 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 javax.net.ssl.SSLContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.AbstractTransporter; +import org.eclipse.aether.spi.connector.transport.GetTask; +import org.eclipse.aether.spi.connector.transport.PeekTask; +import org.eclipse.aether.spi.connector.transport.PutTask; +import org.eclipse.aether.spi.connector.transport.TransportTask; +import org.eclipse.aether.transfer.NoTransporterException; +import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.FileUtils; + +/** + * JDK Transport using {@link HttpClient}. + * + * @since TBD + */ +final class JdkHttpTransporter extends AbstractTransporter { + private static final int MULTIPLE_CHOICES = 300; + + private static final int NOT_FOUND = 404; + + private static final int PRECONDITION_FAILED = 412; + + private static final long MODIFICATION_THRESHOLD = 60L * 1000L; + + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CACHE_CONTROL = "Cache-Control"; + + private static final String CONTENT_LENGTH = "Content-Length"; + + private static final String CONTENT_RANGE = "Content-Range"; + + private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + + private static final String RANGE = "Range"; + + private static final String USER_AGENT = "User-Agent"; + + private static final String LAST_MODIFIED = "Last-Modified"; + + private static final Pattern CONTENT_RANGE_PATTERN = + Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); + + private final URI baseUri; + + private final HttpClient client; + + private final Map headers; + + private final int requestTimeout; + + JdkHttpTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { + try { + URI uri = new URI(repository.getUrl()).parseServerAuthority(); + if (uri.isOpaque()) { + throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); + } + if (uri.getRawFragment() != null || uri.getRawQuery() != null) { + throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); + } + String path = uri.getPath(); + if (path == null) { + path = "/"; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + if (!path.endsWith("/")) { + path = path + "/"; + } + this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path); + } catch (URISyntaxException e) { + throw new NoTransporterException(repository, e.getMessage(), e); + } + + HashMap headers = new HashMap<>(); + String userAgent = ConfigUtils.getString( + session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT); + if (userAgent != null) { + headers.put(USER_AGENT, userAgent); + } + @SuppressWarnings("unchecked") + Map configuredHeaders = (Map) ConfigUtils.getMap( + session, + Collections.emptyMap(), + ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), + ConfigurationProperties.HTTP_HEADERS); + if (configuredHeaders != null) { + configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null)); + } + headers.put(CACHE_CONTROL, "no-cache, no-store"); + + this.requestTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, + ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.REQUEST_TIMEOUT); + + this.headers = headers; + this.client = getOrCreateClient(session, repository); + } + + private URI resolve(TransportTask task) { + return baseUri.resolve(task.getLocation()); + } + + @Override + public int classify(Throwable error) { + if (error instanceof JdkHttpException && ((JdkHttpException) error).getStatusCode() == NOT_FOUND) { + return ERROR_NOT_FOUND; + } + return ERROR_OTHER; + } + + @Override + protected void implPeek(PeekTask task) throws Exception { + HttpRequest.Builder request = HttpRequest.newBuilder() + .uri(resolve(task)) + .timeout(Duration.ofMillis(requestTimeout)) + .method("HEAD", HttpRequest.BodyPublishers.noBody()); + headers.forEach(request::setHeader); + HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + throw new JdkHttpException(response.statusCode()); + } + } + + @Override + protected void implGet(GetTask task) throws Exception { + boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; + HttpResponse response; + + 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"); + } + + response = client.send(request.build(), HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + if (resume && response.statusCode() == PRECONDITION_FAILED) { + resume = false; + continue; + } + throw new JdkHttpException(response.statusCode()); + } + 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); + } + } + } + + 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); + } + } + 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 + } + } + } + Map checksums = extractXChecksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + return; + } + checksums = extractNexus2Checksums(response); + if (checksums != null) { + checksums.forEach(task::setChecksum); + } + } + + @Override + protected void implPut(PutTask task) throws Exception { + HttpRequest.Builder request = + HttpRequest.newBuilder().uri(resolve(task)).timeout(Duration.ofMillis(requestTimeout)); + headers.forEach(request::setHeader); + try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { + utilPut(task, Files.newOutputStream(tempFile.getPath()), true); + request.method("PUT", HttpRequest.BodyPublishers.ofFile(tempFile.getPath())); + + HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() >= MULTIPLE_CHOICES) { + throw new JdkHttpException(response.statusCode()); + } + } + } + + @Override + protected void implClose() { + // nop + } + + private static Map extractXChecksums(HttpResponse response) { + String value; + HashMap result = new HashMap<>(); + // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue("x-checksum-sha1").orElse(null); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue("x-checksum-md5").orElse(null); + if (value != null) { + result.put("MD5", value); + } + if (!result.isEmpty()) { + return result; + } + // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b + value = response.headers().firstValue("x-goog-meta-checksum-sha1").orElse(null); + if (value != null) { + result.put("SHA-1", value); + } + // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 + value = response.headers().firstValue("x-goog-meta-checksum-md5").orElse(null); + if (value != null) { + result.put("MD5", value); + } + + return result.isEmpty() ? null : result; + } + + private static Map extractNexus2Checksums(HttpResponse response) { + // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" + String etag = response.headers().firstValue("ETag").orElse(null); + if (etag != null) { + int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5); + if (start >= 0 && end > start) { + return Collections.singletonMap("SHA-1", etag.substring(start + 5, end)); + } + } + return null; + } + + private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern( + "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + .withZone(ZoneId.of("GMT")); + + /** + * Visible for testing. + */ + static final String HTTP_INSTANCE_KEY_PREFIX = JdkTransporterFactory.class.getName() + ".http."; + + private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + final String instanceKey = HTTP_INSTANCE_KEY_PREFIX + repository.getId(); + + try { + return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { + HashMap authentications = new HashMap<>(); + SSLContext sslContext = null; + try { + try (AuthenticationContext repoAuthContext = + AuthenticationContext.forRepository(session, repository)) { + if (repoAuthContext != null) { + sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class); + + String username = repoAuthContext.get(AuthenticationContext.USERNAME); + String password = repoAuthContext.get(AuthenticationContext.PASSWORD); + + authentications.put( + Authenticator.RequestorType.SERVER, + new PasswordAuthentication(username, password.toCharArray())); + } + } + + if (sslContext == null) { + sslContext = SSLContext.getDefault(); + } + + int connectTimeout = ConfigUtils.getInteger( + session, + ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, + ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), + ConfigurationProperties.CONNECT_TIMEOUT); + + HttpClient.Builder builder = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .followRedirects(HttpClient.Redirect.NORMAL) + .connectTimeout(Duration.ofMillis(connectTimeout)) + .sslContext(sslContext); + + if (repository.getProxy() != null) { + ProxySelector proxy = ProxySelector.of(new InetSocketAddress( + repository.getProxy().getHost(), + repository.getProxy().getPort())); + + builder.proxy(proxy); + try (AuthenticationContext proxyAuthContext = + AuthenticationContext.forProxy(session, repository)) { + if (proxyAuthContext != null) { + String username = proxyAuthContext.get(AuthenticationContext.USERNAME); + String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); + + authentications.put( + Authenticator.RequestorType.PROXY, + new PasswordAuthentication(username, password.toCharArray())); + } + } + } + + if (!authentications.isEmpty()) { + builder.authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return authentications.get(getRequestorType()); + } + }); + } + + return builder.build(); + } catch (NoSuchAlgorithmException e) { + throw new WrapperEx(e); + } + }); + } catch (WrapperEx e) { + throw new NoTransporterException(repository, e.getCause()); + } + } + + private static final class WrapperEx extends RuntimeException { + private WrapperEx(Throwable cause) { + super(cause); + } + } +} diff --git a/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java new file mode 100644 index 000000000..ecb327491 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -0,0 +1,64 @@ +/* + * 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 javax.inject.Named; + +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.spi.connector.transport.Transporter; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.NoTransporterException; + +import static java.util.Objects.requireNonNull; + +/** + * JDK Transport factory. + * + * @since TBD + */ +@Named( JdkTransporterFactory.NAME) +public final class JdkTransporterFactory implements TransporterFactory { + public static final String NAME = "jdk"; + + private float priority = 10.0f; + + @Override + public float getPriority() { + return priority; + } + + public JdkTransporterFactory setPriority( float priority) { + this.priority = priority; + return this; + } + + @Override + public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) + throws NoTransporterException { + requireNonNull(session, "session cannot be null"); + requireNonNull(repository, "repository cannot be null"); + + if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) { + throw new NoTransporterException(repository, "Only HTTP/HTTPS is supported"); + } + + return new JdkHttpTransporter(session, repository); + } +} diff --git a/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java new file mode 100644 index 000000000..8ef1b81a9 --- /dev/null +++ b/maven-resolver-transport-jetty-parent/src/main/java11/org/eclipse/aether/transport/jdk/package-info.java @@ -0,0 +1,26 @@ +// CHECKSTYLE_OFF: RegexpHeader +/* + * 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. + */ +/** + * Support for downloads/uploads via the HTTP and HTTPS protocols. The implementation is backed by + * Java 11 {@link java.net.http.HttpClient}. + * + * @since TBD + */ +package org.eclipse.aether.transport.jdk; diff --git a/pom.xml b/pom.xml index 43d9d3d7f..a92cab22d 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ maven-resolver-connector-basic maven-resolver-transport-classpath maven-resolver-transport-file - maven-resolver-transport-jetty + maven-resolver-transport-jetty-parent maven-resolver-transport-jdk-parent maven-resolver-transport-http maven-resolver-transport-wagon From 1c8324cbcc1cb14a2d96757111c8484e65a55e01 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 21:54:58 +0200 Subject: [PATCH 4/6] WIP --- maven-resolver-transport-jetty/pom.xml | 101 ---- .../jetty/JettyTransporterFactory.java | 60 --- .../transport/jetty/JettyException.java | 37 -- .../transport/jetty/JettyTransporter.java | 450 ------------------ .../jetty/JettyTransporterFactory.java | 64 --- .../aether/transport/jetty/package-info.java | 26 - 6 files changed, 738 deletions(-) delete mode 100644 maven-resolver-transport-jetty/pom.xml delete mode 100644 maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java delete mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java delete mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java delete mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java delete mode 100644 maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java diff --git a/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty/pom.xml deleted file mode 100644 index b239bcaa1..000000000 --- a/maven-resolver-transport-jetty/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - 4.0.0 - - - org.apache.maven.resolver - maven-resolver - 2.0.0-SNAPSHOT - - - maven-resolver-transport-jetty - jar - - Maven Artifact Resolver Transport Jetty - Maven Artifact Transport Jetty Java 11+. - - - org.apache.maven.resolver.transport.jetty - ${Automatic-Module-Name} - - 10.0.17 - - - - - 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.eclipse.jetty - jetty-client - ${jettyVersion} - - - org.eclipse.jetty.http2 - http2-client - ${jettyVersion} - - - org.eclipse.jetty.http2 - http2-http-client-transport - ${jettyVersion} - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - java11 - - compile - - - 11 - true - ${project.basedir}/src/main/java11 - - - - - - - - diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java deleted file mode 100644 index 38f140768..000000000 --- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java +++ /dev/null @@ -1,60 +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.eclipse.aether.transport.jetty; - -import javax.inject.Named; - -import org.eclipse.aether.RepositorySystemSession; -import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; -import org.eclipse.aether.transfer.NoTransporterException; - -import static java.util.Objects.requireNonNull; - -/** - * Jetty Transport factory: on Java 8 is no-op. - * - * @since TBD - */ -@Named(JettyTransporterFactory.NAME) -public final class JettyTransporterFactory implements TransporterFactory { - public static final String NAME = "jetty"; - - private float priority = 15.0f; - - @Override - public float getPriority() { - return priority; - } - - public JettyTransporterFactory setPriority(float priority) { - this.priority = priority; - return this; - } - - @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) - throws NoTransporterException { - requireNonNull(session, "session cannot be null"); - requireNonNull(repository, "repository cannot be null"); - - throw new NoTransporterException(repository, "Jetty Transport needs Java11+"); - } -} diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java deleted file mode 100644 index 3cd907bf2..000000000 --- a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyException.java +++ /dev/null @@ -1,37 +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.eclipse.aether.transport.jetty; - -/** - * Exception thrown by {@link JettyTransporter} in case of errors. - * - * @since TBD - */ -final class JettyException extends Exception { - private final int statusCode; - - JettyException(int statusCode) { - super("HTTP Status: " + statusCode); - this.statusCode = statusCode; - } - - public int getStatusCode() { - return statusCode; - } -} diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java deleted file mode 100644 index 69bb1c060..000000000 --- a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporter.java +++ /dev/null @@ -1,450 +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.eclipse.aether.transport.jetty; - -import javax.net.ssl.SSLContext; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.aether.ConfigurationProperties; -import org.eclipse.aether.RepositorySystemSession; -import org.eclipse.aether.repository.AuthenticationContext; -import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.AbstractTransporter; -import org.eclipse.aether.spi.connector.transport.GetTask; -import org.eclipse.aether.spi.connector.transport.PeekTask; -import org.eclipse.aether.spi.connector.transport.PutTask; -import org.eclipse.aether.spi.connector.transport.TransportTask; -import org.eclipse.aether.transfer.NoTransporterException; -import org.eclipse.aether.util.ConfigUtils; -import org.eclipse.aether.util.FileUtils; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpProxy; -import org.eclipse.jetty.client.api.Authentication; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; -import org.eclipse.jetty.client.http.HttpClientConnectionFactory; -import org.eclipse.jetty.client.util.BasicAuthentication; -import org.eclipse.jetty.client.util.InputStreamResponseListener; -import org.eclipse.jetty.client.util.PathRequestContent; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http2.client.HTTP2Client; -import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; -import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -/** - * A transporter for HTTP/HTTPS. - * - * @since TBD - */ -final class JettyTransporter extends AbstractTransporter { - private static final int MULTIPLE_CHOICES = 300; - - private static final int NOT_FOUND = 404; - - private static final int PRECONDITION_FAILED = 412; - - private static final long MODIFICATION_THRESHOLD = 60L * 1000L; - - private static final String ACCEPT_ENCODING = "Accept-Encoding"; - - private static final String CONTENT_LENGTH = "Content-Length"; - - private static final String CONTENT_RANGE = "Content-Range"; - - private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; - - private static final String RANGE = "Range"; - - private static final String USER_AGENT = "User-Agent"; - - private static final Pattern CONTENT_RANGE_PATTERN = - Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*"); - - private final URI baseUri; - - private final HttpClient client; - - private final int requestTimeout; - - private final Map headers; - - JettyTransporter(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { - try { - URI uri = new URI(repository.getUrl()).parseServerAuthority(); - if (uri.isOpaque()) { - throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); - } - if (uri.getRawFragment() != null || uri.getRawQuery() != null) { - throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); - } - String path = uri.getPath(); - if (path == null) { - path = "/"; - } - if (!path.startsWith("/")) { - path = "/" + path; - } - if (!path.endsWith("/")) { - path = path + "/"; - } - this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path); - } catch (URISyntaxException e) { - throw new NoTransporterException(repository, e.getMessage(), e); - } - - HashMap headers = new HashMap<>(); - String userAgent = ConfigUtils.getString( - session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT); - if (userAgent != null) { - headers.put(USER_AGENT, userAgent); - } - @SuppressWarnings("unchecked") - Map configuredHeaders = (Map) ConfigUtils.getMap( - session, - Collections.emptyMap(), - ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), - ConfigurationProperties.HTTP_HEADERS); - if (configuredHeaders != null) { - configuredHeaders.forEach((k, v) -> headers.put(String.valueOf(k), v != null ? String.valueOf(v) : null)); - } - - this.headers = headers; - - this.requestTimeout = ConfigUtils.getInteger( - session, - ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, - ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), - ConfigurationProperties.REQUEST_TIMEOUT); - - this.client = getOrCreateClient(session, repository); - } - - private URI resolve(TransportTask task) { - return baseUri.resolve(task.getLocation()); - } - - @Override - public int classify(Throwable error) { - if (error instanceof JettyException && ((JettyException) error).getStatusCode() == NOT_FOUND) { - return ERROR_NOT_FOUND; - } - return ERROR_OTHER; - } - - @Override - protected void implPeek(PeekTask task) throws Exception { - Request request = client.newRequest(resolve(task)) - .timeout(requestTimeout, TimeUnit.MILLISECONDS) - .method("HEAD"); - request.headers(m -> headers.forEach(m::add)); - Response response = request.send(); - if (response.getStatus() >= MULTIPLE_CHOICES) { - throw new JettyException(response.getStatus()); - } - } - - @Override - protected void implGet(GetTask task) throws Exception { - boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null; - Response response; - InputStreamResponseListener listener; - - while (true) { - Request request = client.newRequest(resolve(task)) - .timeout(requestTimeout, TimeUnit.MILLISECONDS) - .method("GET"); - request.headers(m -> headers.forEach(m::add)); - - if (resume) { - long resumeOffset = task.getResumeOffset(); - request.headers(h -> { - h.add(RANGE, "bytes=" + resumeOffset + '-'); - h.addDateField(IF_UNMODIFIED_SINCE, task.getDataFile().lastModified() - MODIFICATION_THRESHOLD); - h.remove(HttpHeader.ACCEPT_ENCODING); - h.add(ACCEPT_ENCODING, "identity"); - }); - } - - listener = new InputStreamResponseListener(); - request.send(listener); - try { - response = listener.get(requestTimeout, TimeUnit.MILLISECONDS); - } catch (ExecutionException e) { - Throwable t = e.getCause(); - if (t instanceof Exception) { - throw (Exception) t; - } else { - throw new RuntimeException(t); - } - } - if (response.getStatus() >= MULTIPLE_CHOICES) { - if (resume && response.getStatus() == PRECONDITION_FAILED) { - resume = false; - continue; - } - throw new JettyException(response.getStatus()); - } - break; - } - - long offset = 0L, length = response.getHeaders().getLongField(CONTENT_LENGTH); - if (resume) { - String range = response.getHeaders().get(CONTENT_RANGE); - 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 = listener.getInputStream()) { - 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); - } - } - try (InputStream is = listener.getInputStream()) { - utilGet(task, is, true, length, downloadResumed); - } - tempFile.move(); - } finally { - task.setDataFile(dataFile); - } - } - Map checksums = extractXChecksums(response); - if (checksums != null) { - checksums.forEach(task::setChecksum); - return; - } - checksums = extractNexus2Checksums(response); - if (checksums != null) { - checksums.forEach(task::setChecksum); - } - } - - private static Map extractXChecksums(Response response) { - String value; - HashMap result = new HashMap<>(); - // Central style: x-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b - value = response.getHeaders().get("x-checksum-sha1"); - if (value != null) { - result.put("SHA-1", value); - } - // Central style: x-checksum-md5: 9ad0d8e3482767c122e85f83567b8ce6 - value = response.getHeaders().get("x-checksum-md5"); - if (value != null) { - result.put("MD5", value); - } - if (!result.isEmpty()) { - return result; - } - // Google style: x-goog-meta-checksum-sha1: c74edb60ca2a0b57ef88d9a7da28f591e3d4ce7b - value = response.getHeaders().get("x-goog-meta-checksum-sha1"); - if (value != null) { - result.put("SHA-1", value); - } - // Central style: x-goog-meta-checksum-sha1: 9ad0d8e3482767c122e85f83567b8ce6 - value = response.getHeaders().get("x-goog-meta-checksum-md5"); - if (value != null) { - result.put("MD5", value); - } - - return result.isEmpty() ? null : result; - } - - private static Map extractNexus2Checksums(Response response) { - // Nexus-style, ETag: "{SHA1{d40d68ba1f88d8e9b0040f175a6ff41928abd5e7}}" - String etag = response.getHeaders().get("ETag"); - if (etag != null) { - int start = etag.indexOf("SHA1{"), end = etag.indexOf("}", start + 5); - if (start >= 0 && end > start) { - return Collections.singletonMap("SHA-1", etag.substring(start + 5, end)); - } - } - return null; - } - - @Override - protected void implPut(PutTask task) throws Exception { - Request request = client.newRequest(resolve(task)).method("PUT").timeout(requestTimeout, TimeUnit.MILLISECONDS); - request.headers(m -> headers.forEach(m::add)); - try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) { - utilPut(task, Files.newOutputStream(tempFile.getPath()), true); - request.body(new PathRequestContent(tempFile.getPath())); - - Response response; - try { - response = request.send(); - } catch (ExecutionException e) { - Throwable t = e.getCause(); - if (t instanceof Exception) { - throw (Exception) t; - } else { - throw new RuntimeException(t); - } - } - if (response.getStatus() >= MULTIPLE_CHOICES) { - throw new JettyException(response.getStatus()); - } - } - } - - @Override - protected void implClose() { - // noop - } - - /** - * Visible for testing. - */ - static final String JETTY_INSTANCE_KEY_PREFIX = JettyTransporterFactory.class.getName() + ".jetty."; - - @SuppressWarnings("checkstyle:methodlength") - private static HttpClient getOrCreateClient(RepositorySystemSession session, RemoteRepository repository) - throws NoTransporterException { - - final String instanceKey = JETTY_INSTANCE_KEY_PREFIX + repository.getId(); - - try { - return (HttpClient) session.getData().computeIfAbsent(instanceKey, () -> { - SSLContext sslContext = null; - BasicAuthentication basicAuthentication = null; - try { - try (AuthenticationContext repoAuthContext = - AuthenticationContext.forRepository(session, repository)) { - if (repoAuthContext != null) { - sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class); - - String username = repoAuthContext.get(AuthenticationContext.USERNAME); - String password = repoAuthContext.get(AuthenticationContext.PASSWORD); - - basicAuthentication = new BasicAuthentication( - URI.create(repository.getUrl()), Authentication.ANY_REALM, username, password); - } - } - - if (sslContext == null) { - sslContext = SSLContext.getDefault(); - } - - int connectTimeout = ConfigUtils.getInteger( - session, - ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, - ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), - ConfigurationProperties.CONNECT_TIMEOUT); - - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - sslContextFactory.setSslContext(sslContext); - - ClientConnector clientConnector = new ClientConnector(); - clientConnector.setSslContextFactory(sslContextFactory); - - HTTP2Client http2Client = new HTTP2Client(clientConnector); - ClientConnectionFactoryOverHTTP2.HTTP2 http2 = - new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); - - HttpClientTransportDynamic transport; - if ("https".equalsIgnoreCase(repository.getProtocol())) { - transport = new HttpClientTransportDynamic( - clientConnector, http2, HttpClientConnectionFactory.HTTP11); // HTTPS, prefer H2 - } else { - transport = new HttpClientTransportDynamic( - clientConnector, - HttpClientConnectionFactory.HTTP11, - http2); // plaintext HTTP, H2 cannot be used - } - - HttpClient httpClient = new HttpClient(transport); - httpClient.setConnectTimeout(connectTimeout); - httpClient.setFollowRedirects(true); - httpClient.setMaxRedirects(2); - - httpClient.setUserAgentField(null); // we manage it - - if (basicAuthentication != null) { - httpClient.getAuthenticationStore().addAuthentication(basicAuthentication); - } - - if (repository.getProxy() != null) { - HttpProxy proxy = new HttpProxy( - repository.getProxy().getHost(), - repository.getProxy().getPort()); - - httpClient.getProxyConfiguration().addProxy(proxy); - try (AuthenticationContext proxyAuthContext = - AuthenticationContext.forProxy(session, repository)) { - if (proxyAuthContext != null) { - String username = proxyAuthContext.get(AuthenticationContext.USERNAME); - String password = proxyAuthContext.get(AuthenticationContext.PASSWORD); - - BasicAuthentication proxyAuthentication = new BasicAuthentication( - proxy.getURI(), Authentication.ANY_REALM, username, password); - - httpClient.getAuthenticationStore().addAuthentication(proxyAuthentication); - } - } - } - httpClient.start(); - return httpClient; - } catch (Exception e) { - throw new WrapperEx(e); - } - }); - } catch (WrapperEx e) { - throw new NoTransporterException(repository, e.getCause()); - } - } - - private static final class WrapperEx extends RuntimeException { - private WrapperEx(Throwable cause) { - super(cause); - } - } -} \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java deleted file mode 100644 index ac5166802..000000000 --- a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java +++ /dev/null @@ -1,64 +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.eclipse.aether.transport.jetty; - -import javax.inject.Named; - -import org.eclipse.aether.RepositorySystemSession; -import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.spi.connector.transport.Transporter; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; -import org.eclipse.aether.transfer.NoTransporterException; - -import static java.util.Objects.requireNonNull; - -/** - * A transporter factory for repositories using the {@code http:} or {@code https:} protocol. - * - * @since TBD - */ -@Named(JettyTransportFactory.NAME) -public final class JettyTransporterFactory implements TransporterFactory { - public static final String NAME = "jetty"; - - private float priority = 15.0f; - - @Override - public float getPriority() { - return priority; - } - - public JettyTransporterFactory setPriority(float priority) { - this.priority = priority; - return this; - } - - @Override - public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) - throws NoTransporterException { - requireNonNull(session, "session cannot be null"); - requireNonNull(repository, "repository cannot be null"); - - if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) { - throw new NoTransporterException(repository); - } - - return new JettyTransporter(session, repository); - } -} \ No newline at end of file diff --git a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java b/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java deleted file mode 100644 index 367c20573..000000000 --- a/maven-resolver-transport-jetty/src/main/java11/org/eclipse/aether/transport/jetty/package-info.java +++ /dev/null @@ -1,26 +0,0 @@ -// CHECKSTYLE_OFF: RegexpHeader -/* - * 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. - */ -/** - * Support for downloads/uploads via the HTTP and HTTPS protocols. The implementation is backed by - * Eclipse Jetty. - * - * @since TBD - */ -package org.eclipse.aether.transport.jetty; From 5ce7bada048a4af65addcb1edb172c02355c1167 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 22:03:19 +0200 Subject: [PATCH 5/6] WIP --- .../maven-resolver-demo-snippets/pom.xml | 2 ++ .../maven-resolver-transport-jetty-11/pom.xml | 1 - .../maven-resolver-transport-jetty/pom.xml | 19 +++++++++++++++++++ maven-resolver-transport-jetty-parent/pom.xml | 4 ++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml index e07adfafa..8f7f22c0c 100644 --- a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml +++ b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml @@ -34,6 +34,8 @@ org.apache.maven.resolver.demo.snippets + + 11 diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml index 760bc529d..c407686f2 100644 --- a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-11/pom.xml @@ -37,7 +37,6 @@ ${Automatic-Module-Name} 11 - 10.0.17 diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml index c791b3ef7..fd01a6eb6 100644 --- a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty/pom.xml @@ -35,6 +35,9 @@ org.apache.maven.resolver.transport.jetty ${Automatic-Module-Name} + + + 11 @@ -56,6 +59,22 @@ provided true + + + org.eclipse.jetty + jetty-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-client + ${jettyVersion} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jettyVersion} + diff --git a/maven-resolver-transport-jetty-parent/pom.xml b/maven-resolver-transport-jetty-parent/pom.xml index 5d600f36a..6d9f243ec 100644 --- a/maven-resolver-transport-jetty-parent/pom.xml +++ b/maven-resolver-transport-jetty-parent/pom.xml @@ -38,4 +38,8 @@ maven-resolver-transport-jetty + + 10.0.17 + + From 1c1064d41eac328f6a8892532a4ca1cf774918e8 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Wed, 18 Oct 2023 22:10:10 +0200 Subject: [PATCH 6/6] Both Java8 implementations are no-op. Supress them. --- .../org/eclipse/aether/transport/jdk/JdkTransporterFactory.java | 2 +- .../eclipse/aether/transport/jetty/JettyTransporterFactory.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java index aa015d2b2..80dd1d656 100644 --- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java +++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java @@ -37,7 +37,7 @@ public final class JdkTransporterFactory implements TransporterFactory { public static final String NAME = "jdk"; - private float priority = 10.0f; + private float priority = Float.MIN_VALUE; @Override public float getPriority() { diff --git a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java index 38f140768..bbb7f3988 100644 --- a/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java +++ b/maven-resolver-transport-jetty-parent/maven-resolver-transport-jetty-8/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporterFactory.java @@ -37,7 +37,7 @@ public final class JettyTransporterFactory implements TransporterFactory { public static final String NAME = "jetty"; - private float priority = 15.0f; + private float priority = Float.MIN_VALUE; @Override public float getPriority() {