merge of the current 3.0 into the 3.1
merge of the current 3.0 into the 3.1
diff --git a/NOTICE.md b/NOTICE.md
index 1935ed0..4f33066 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -95,7 +95,7 @@
* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS
* Copyright: Eric Rowell
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
index 949003f..7f5bb9b 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -91,7 +91,7 @@
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
- notifyResponse();
+ notifyResponse(ctx);
}
@Override
@@ -107,7 +107,7 @@
}
}
- protected void notifyResponse() {
+ protected void notifyResponse(ChannelHandlerContext ctx) {
if (jerseyResponse != null) {
ClientResponse cr = jerseyResponse;
jerseyResponse = null;
@@ -146,6 +146,7 @@
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
+ ctx.close();
if (redirectController.prepareRedirect(newReq, cr)) {
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
@@ -227,7 +228,7 @@
if (msg instanceof LastHttpContent) {
responseDone.complete(null);
- notifyResponse();
+ notifyResponse(ctx);
}
}
}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
index bdede02..6bee4d3 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,112 +16,133 @@
package org.glassfish.jersey.netty.connector;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.handler.codec.http.DefaultFullHttpRequest;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
-import io.netty.handler.codec.http.HttpUtil;
-import org.glassfish.jersey.client.ClientRequest;
+import io.netty.handler.codec.http.LastHttpContent;
import jakarta.ws.rs.ProcessingException;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;
public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter {
- private boolean isExpected;
+ private ExpectationState currentState = ExpectationState.IDLE;
- private static final List<HttpResponseStatus> statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE,
- HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED,
- HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+ private static final List<HttpResponseStatus> finalErrorStatuses = Arrays.asList(HttpResponseStatus.UNAUTHORIZED,
+ HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+ private static final List<HttpResponseStatus> reSendErrorStatuses = Arrays.asList(
+ HttpResponseStatus.METHOD_NOT_ALLOWED,
+ HttpResponseStatus.EXPECTATION_FAILED);
- private CompletableFuture<HttpResponseStatus> expectedFuture = new CompletableFuture<>();
+ private static final List<HttpResponseStatus> statusesToBeConsidered = new ArrayList<>(reSendErrorStatuses);
+
+ static {
+ statusesToBeConsidered.addAll(finalErrorStatuses);
+ statusesToBeConsidered.add(HttpResponseStatus.CONTINUE);
+ }
+
+ private HttpResponseStatus status = null;
+
+ private CountDownLatch latch = null;
+
+ private boolean propagateLastMessage = false;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- if (isExpected && msg instanceof HttpResponse) {
- final HttpResponse response = (HttpResponse) msg;
- if (statusesToBeConsidered.contains(response.status())) {
- expectedFuture.complete(response.status());
- }
- if (!HttpResponseStatus.CONTINUE.equals(response.status())) {
+
+ if (checkExpectResponse(msg) || checkInvalidExpect(msg)) {
+ currentState = ExpectationState.AWAITING;
+ }
+ switch (currentState) {
+ case AWAITING:
+ final HttpResponse response = (HttpResponse) msg;
+ status = response.status();
+ boolean handshakeDone = processErrorStatuses(status) || msg instanceof FullHttpMessage;
+ currentState = (handshakeDone) ? ExpectationState.IDLE : ExpectationState.FINISHING;
+ processLatch();
+ return;
+ case FINISHING:
+ if (msg instanceof LastHttpContent) {
+ currentState = ExpectationState.IDLE;
+ if (propagateLastMessage) {
+ propagateLastMessage = false;
+ ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+ }
+ }
+ return;
+ default:
ctx.fireChannelRead(msg); //bypass the message to the next handler in line
- } else {
- ctx.pipeline().remove(JerseyExpectContinueHandler.class);
- }
- } else {
- if (!isExpected
- && ctx.pipeline().context(JerseyExpectContinueHandler.class) != null) {
- ctx.pipeline().remove(JerseyExpectContinueHandler.class);
- }
- ctx.fireChannelRead(msg); //bypass the message to the next handler in line
}
}
- CompletableFuture<HttpResponseStatus> processExpect100ContinueRequest(HttpRequest nettyRequest,
- ClientRequest jerseyRequest,
- Channel ch,
- Integer timeout)
- throws InterruptedException, ExecutionException, TimeoutException {
- //check for 100-Continue presence/availability
- final Expect100ContinueConnectorExtension expect100ContinueExtension
- = new Expect100ContinueConnectorExtension();
-
- final DefaultFullHttpRequest nettyRequestHeaders =
- new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri());
- nettyRequestHeaders.headers().setAll(nettyRequest.headers());
-
- if (!nettyRequestHeaders.headers().contains(HttpHeaderNames.HOST)) {
- nettyRequestHeaders.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+ private boolean checkExpectResponse(Object msg) {
+ if (currentState == ExpectationState.IDLE && latch != null && msg instanceof HttpResponse) {
+ return statusesToBeConsidered.contains(((HttpResponse) msg).status());
}
-
- //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be
- //enriched with the 'Expect:100-continue' header.
- expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders);
-
- final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders))
- // Send only head of the HTTP request enriched with Expect:100-continue header.
- ? ch.writeAndFlush(nettyRequestHeaders)
- // Expect:100-Continue either is not supported or is turned off
- : null;
- isExpected = expect100ContinueFuture != null;
- if (!isExpected) {
- ch.pipeline().remove(JerseyExpectContinueHandler.class);
- } else {
- final HttpResponseStatus status = expectedFuture
- .get(timeout, TimeUnit.MILLISECONDS);
-
- processExpectationStatus(status);
- }
- return expectedFuture;
+ return false;
}
- private void processExpectationStatus(HttpResponseStatus status)
- throws TimeoutException {
+ private boolean checkInvalidExpect(Object msg) {
+ return (ExpectationState.IDLE.equals(currentState)
+ && msg instanceof HttpResponse
+ && (HttpResponseStatus.CONTINUE.equals(((HttpResponse) msg).status())
+ || reSendErrorStatuses.contains(((HttpResponse) msg).status()))
+ );
+ }
+
+ boolean processErrorStatuses(HttpResponseStatus status) {
+ if (reSendErrorStatuses.contains(status)) {
+ propagateLastMessage = true;
+ }
+ return (finalErrorStatuses.contains(status));
+ }
+
+ void processExpectationStatus()
+ throws TimeoutException, IOException {
+ if (status == null) {
+ throw new TimeoutException(); // continue without expectations
+ }
if (!statusesToBeConsidered.contains(status)) {
throw new ProcessingException(LocalizationMessages
.UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
}
- if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) {
- isExpected = false;
- throw new TimeoutException(); // continue without expectations
+
+ if (finalErrorStatuses.contains(status)) {
+ throw new IOException(LocalizationMessages
+ .EXPECT_100_CONTINUE_FAILED_REQUEST_FAILED(), null);
}
- if (!HttpResponseStatus.CONTINUE.equals(status)) {
- throw new ProcessingException(LocalizationMessages
- .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+
+ if (reSendErrorStatuses.contains(status)) {
+ throw new TimeoutException(LocalizationMessages
+ .EXPECT_100_CONTINUE_FAILED_REQUEST_SHOULD_BE_RESENT()); // Re-send request without expectations
+ }
+
+ }
+
+ void resetHandler() {
+ latch = null;
+ }
+
+ void attachCountDownLatch(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ private void processLatch() {
+ if (latch != null) {
+ latch.countDown();
}
}
- boolean isExpected() {
- return isExpected;
+ private enum ExpectationState {
+ AWAITING,
+ FINISHING,
+ IDLE
}
-}
\ No newline at end of file
+}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index a4622b8..a65560c 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -17,9 +17,7 @@
package org.glassfish.jersey.netty.connector;
import java.io.IOException;
-import java.io.InterruptedIOException;
import java.io.OutputStream;
-import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
@@ -34,7 +32,6 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -70,6 +67,7 @@
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.ProxyHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
@@ -257,6 +255,8 @@
}
}
+ final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
+
if (chan == null) {
Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
Bootstrap b = new Bootstrap();
@@ -329,8 +329,8 @@
final Integer maxInitialLineLength = ClientProperties.getValue(config.getProperties(),
NettyClientProperties.MAX_INITIAL_LINE_LENGTH,
NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH);
-
p.addLast(new HttpClientCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize));
+ p.addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
p.addLast(new ChunkedWriteHandler());
p.addLast(new HttpContentDecompressor());
}
@@ -359,11 +359,10 @@
final Channel ch = chan;
JerseyClientHandler clientHandler =
new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this);
- final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
+
// read timeout makes sense really as an inactivity timeout
ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
- ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
responseDone.whenComplete((_r, th) -> {
@@ -446,22 +445,11 @@
// // Set later after the entity is "written"
// break;
}
- try {
- expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest,
- ch, expect100ContinueTimeout);
- } catch (ExecutionException e) {
- responseDone.completeExceptionally(e);
- } catch (TimeoutException e) {
- //Expect:100-continue allows timeouts by the spec
- //just removing the pipeline from processing
- if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) {
- ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER);
- }
- }
final CountDownLatch headersSet = new CountDownLatch(1);
final CountDownLatch contentLengthSet = new CountDownLatch(1);
+
jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
@Override
public OutputStream getOutputStream(int contentLength) throws IOException {
@@ -486,7 +474,6 @@
try {
jerseyRequest.writeEntity();
-
if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength());
contentLengthSet.countDown();
@@ -506,12 +493,36 @@
});
headersSet.await();
- if (!expect100ContinueHandler.isExpected()) {
- // Send the HTTP request. Expect:100-continue processing is not applicable
- // in this case.
+ new Expect100ContinueConnectorExtension().invoke(jerseyRequest, nettyRequest);
+
+ boolean continueExpected = HttpUtil.is100ContinueExpected(nettyRequest);
+ boolean expectationsFailed = false;
+
+ if (continueExpected) {
+ final CountDownLatch expect100ContinueLatch = new CountDownLatch(1);
+ expect100ContinueHandler.attachCountDownLatch(expect100ContinueLatch);
+ //send expect request, sync and wait till either response or timeout received
entityWriter.writeAndFlush(nettyRequest);
+ expect100ContinueLatch.await(expect100ContinueTimeout, TimeUnit.MILLISECONDS);
+ try {
+ expect100ContinueHandler.processExpectationStatus();
+ } catch (TimeoutException e) {
+ //Expect:100-continue allows timeouts by the spec
+ //so, send request directly without Expect header.
+ expectationsFailed = true;
+ } finally {
+ //restore request and handler to the original state.
+ HttpUtil.set100ContinueExpected(nettyRequest, false);
+ expect100ContinueHandler.resetHandler();
+ }
}
+ if (!continueExpected || expectationsFailed) {
+ if (expectationsFailed) {
+ ch.pipeline().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).sync();
+ }
+ entityWriter.writeAndFlush(nettyRequest);
+ }
if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
} else {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
index 4ffe52e..baa9118 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/JerseyChunkedInput.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -68,7 +68,8 @@
ByteBuffer peek = queue.peek();
if ((peek != null && peek == VOID)) {
- queue.remove(); // VOID from the top.
+ //required for JDK 11 and netty.version = 4.1.121.Final
+ queue.poll(); // VOID from the top.
open = false;
removeCloseListener();
return true;
diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
index 7d6f9fc..dd5de49 100644
--- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
+++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2016, 2023 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,3 +23,5 @@
redirect.infinite.loop="Infinite loop in chained redirects detected."
redirect.limit.reached="Max chained redirect limit ({0}) exceeded."
unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}").
+expect.100.continue.failed.request.should.be.resent=Expect 100-continue failed. Request should be resent.
+expect.100.continue.failed.request.failed=Expect 100-continue failed. Request failed.
diff --git a/containers/jdk-http/pom.xml b/containers/jdk-http/pom.xml
index a1b3b99..abb6ca0 100644
--- a/containers/jdk-http/pom.xml
+++ b/containers/jdk-http/pom.xml
@@ -34,12 +34,6 @@
<dependencies>
<dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <scope>test</scope>
- <version>${commons.io.version}</version>
- </dependency>
- <dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
index 2b912d8..a0d978b 100644
--- a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
+++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -30,9 +30,9 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
-import org.apache.commons.io.IOUtils;
import org.glassfish.jersey.SslConfigurator;
import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.message.internal.ReaderWriter;
import org.glassfish.jersey.server.ResourceConfig;
import org.junit.jupiter.api.AfterEach;
@@ -195,9 +195,9 @@
final SslConfigurator sslConfigClient = SslConfigurator.newInstance()
- .trustStoreBytes(IOUtils.toByteArray(trustStore))
+ .trustStoreBytes(ReaderWriter.readFromAsBytes(trustStore))
.trustStorePassword(TRUSTSTORE_CLIENT_PWD)
- .keyStoreBytes(IOUtils.toByteArray(keyStore))
+ .keyStoreBytes(ReaderWriter.readFromAsBytes(keyStore))
.keyPassword(KEYSTORE_CLIENT_PWD);
return sslConfigClient.createSSLContext();
@@ -208,9 +208,9 @@
final InputStream keyStore = JdkHttpsServerTest.class.getResourceAsStream(KEYSTORE_SERVER_FILE);
final SslConfigurator sslConfigServer = SslConfigurator.newInstance()
- .keyStoreBytes(IOUtils.toByteArray(keyStore))
+ .keyStoreBytes(ReaderWriter.readFromAsBytes(keyStore))
.keyPassword(KEYSTORE_SERVER_PWD)
- .trustStoreBytes(IOUtils.toByteArray(trustStore))
+ .trustStoreBytes(ReaderWriter.readFromAsBytes(trustStore))
.trustStorePassword(TRUSTSTORE_SERVER_PWD);
return sslConfigServer.createSSLContext();
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
index e6f5155..f35c720 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/WebComponent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -38,6 +38,7 @@
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import jakarta.servlet.ServletInputStream;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.GenericType;
@@ -425,13 +426,18 @@
try {
requestContext.setEntityStream(new InputStreamWrapper() {
+
+ private ServletInputStream wrappedStream;
@Override
protected InputStream getWrapped() {
- try {
- return servletRequest.getInputStream();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ if (wrappedStream == null) {
+ try {
+ wrappedStream = servletRequest.getInputStream();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
+ return wrappedStream;
}
});
} catch (UncheckedIOException e) {
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
index 9876c01..6538a9e 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -115,6 +115,10 @@
@Override
public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse responseContext)
throws ContainerException {
+ if (asyncExt.isCompleted()) {
+ return null;
+ }
+
this.responseContext.complete(responseContext);
// first set the content length, so that if headers have an explicit value, it takes precedence over this one
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
index 5825375..2652e62 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -42,4 +42,17 @@
* Invoked upon a response writing completion when the response write is either committed or canceled.
*/
public void complete();
+
+ /**
+ * <p>
+ * Return {@code true} when the AsyncContext is completed, such as when {@link #complete()} has been called.
+ * </p>
+ * <p>
+ * For compatibility, the default is {@code false}.
+ * </p>
+ * @return {@code true} when the AsyncContext is completed.
+ */
+ public default boolean isCompleted() {
+ return false;
+ }
}
diff --git a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
index efe248e..89fa99a 100644
--- a/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
+++ b/containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,12 +16,15 @@
package org.glassfish.jersey.servlet.async;
+import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.servlet.AsyncContext;
+import jakarta.servlet.AsyncEvent;
+import jakarta.servlet.AsyncListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -70,7 +73,32 @@
public void suspend() throws IllegalStateException {
// Suspend only if not completed and not suspended before.
if (!completed.get() && asyncContextRef.get() == null) {
- asyncContextRef.set(getAsyncContext());
+ final AsyncContext asyncContext = getAsyncContext();
+ asyncContext.addListener(new CompletedAsyncContextListener());
+ asyncContextRef.set(asyncContext);
+ }
+ }
+
+ private class CompletedAsyncContextListener implements AsyncListener {
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException {
+ complete();
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException {
+
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException {
+ complete();
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException {
+
}
}
@@ -102,5 +130,10 @@
asyncContext.complete();
}
}
+
+ @Override
+ public boolean isCompleted() {
+ return completed.get();
+ }
}
}
diff --git a/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java b/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java
new file mode 100644
index 0000000..220d832
--- /dev/null
+++ b/containers/jersey-servlet/src/test/java/org/glassfish/jersey/servlet/async/AsyncContextClosedTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.servlet.async;
+
+import org.glassfish.jersey.server.spi.ContainerResponseWriter;
+import org.glassfish.jersey.servlet.internal.ResponseWriter;
+import org.junit.jupiter.api.Test;
+
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.AsyncListener;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AsyncContextClosedTest {
+ @Test
+ public void testClosedAsyncContext() {
+ List<AsyncListener> asyncListeners = new ArrayList<>(1);
+ AsyncContext async = (AsyncContext) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{AsyncContext.class}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ switch (method.getName()) {
+ case "addListener":
+ asyncListeners.add((AsyncListener) args[0]);
+ break;
+ case "complete":
+ asyncListeners.forEach((asyncListener -> {
+ try {
+ asyncListener.onComplete(null);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+ return null;
+ }
+ });
+
+ HttpServletRequest request = (HttpServletRequest) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{HttpServletRequest.class}, new InvocationHandler() {
+ boolean asyncStarted = false;
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ switch (method.getName()) {
+ case "isAsyncStarted":
+ return asyncStarted;
+ case "startAsync":
+ asyncStarted = true;
+ return async;
+ case "getAsyncContext":
+ return async;
+
+ }
+ return null;
+ }
+ });
+
+ HttpServletResponse response = (HttpServletResponse) Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class[]{HttpServletResponse.class}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return null;
+ }
+ });
+
+ // Create writer
+ ResponseWriter writer = new ResponseWriter(false, false, response,
+ new AsyncContextDelegateProviderImpl().createDelegate(request, response),
+ Executors.newSingleThreadScheduledExecutor());
+ writer.suspend(10, TimeUnit.SECONDS, new ContainerResponseWriter.TimeoutHandler() {
+ @Override
+ public void onTimeout(ContainerResponseWriter responseWriter) {
+ throw new IllegalStateException();
+ }
+ });
+ // Simulate completion by the Servlet Container;
+ request.getAsyncContext().complete();
+ // Check write is ignored
+ writer.writeResponseStatusAndHeaders(10, null);
+ }
+}
diff --git a/core-client/pom.xml b/core-client/pom.xml
index 4630b07..34a1e33 100644
--- a/core-client/pom.xml
+++ b/core-client/pom.xml
@@ -144,6 +144,12 @@
</dependency>
<dependency>
+ <groupId>jakarta.xml.bind</groupId>
+ <artifactId>jakarta.xml.bind-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${project.version}</version>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
index 9491741..698fb39 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -22,6 +22,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
@@ -62,8 +63,12 @@
* @param response JAX-RS response to be used to initialize the response context.
*/
public ClientResponse(final ClientRequest requestContext, final Response response) {
- this(response.getStatusInfo(), requestContext);
- this.headers(OutboundJaxrsResponse.from(response, requestContext.getConfiguration()).getContext().getStringHeaders());
+ super(requestContext.getConfiguration(),
+ OutboundJaxrsResponse.from(response, requestContext.getConfiguration()).getContext().getStringHeaders(),
+ false
+ );
+ this.requestContext = requestContext;
+ init(response.getStatusInfo(), requestContext, requestContext.getUri());
final Object entity = response.getEntity();
if (entity != null) {
@@ -77,18 +82,19 @@
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream stream = null;
try {
- try {
- stream = requestContext.getWorkers().writeTo(
- entity, entity.getClass(), null, null, response.getMediaType(),
- response.getMetadata(), requestContext.getPropertiesDelegate(), baos,
- Collections.<WriterInterceptor>emptyList());
- } finally {
- if (stream != null) {
- stream.close();
- }
- }
+ final Type t = response instanceof OutboundJaxrsResponse
+ ? ((OutboundJaxrsResponse) response).getContext().getEntityType()
+ : null;
+ stream = requestContext.getWorkers().writeTo(
+ entity, entity.getClass(), t, null, response.getMediaType(),
+ response.getMetadata(), requestContext.getPropertiesDelegate(), baos,
+ Collections.<WriterInterceptor>emptyList());
} catch (IOException e) {
// ignore
+ } finally {
+ if (stream != null) {
+ stream.close();
+ }
}
byteArrayInputStream = new ByteArrayInputStream(baos.toByteArray());
@@ -120,10 +126,13 @@
*/
public ClientResponse(Response.StatusType status, ClientRequest requestContext, URI resolvedRequestUri) {
super(requestContext.getConfiguration());
+ this.requestContext = requestContext;
+ init(status, requestContext, resolvedRequestUri);
+ }
+
+ private void init(Response.StatusType status, ClientRequest requestContext, URI resolvedRequestUri) {
this.status = status;
this.resolvedUri = resolvedRequestUri;
- this.requestContext = requestContext;
-
setWorkers(requestContext.getWorkers());
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
index 9637066..d38fc2b 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -17,8 +17,8 @@
package org.glassfish.jersey.client.authentication;
import java.util.Base64;
+import java.util.List;
import java.util.Locale;
-import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.ws.rs.client.ClientRequestContext;
@@ -96,20 +96,22 @@
* @throws ResponseAuthenticationException in case that basic credentials missing or are in invalid format
*/
public boolean filterResponseAndAuthenticate(ClientRequestContext request, ClientResponseContext response) {
- final String authenticate = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
- if (authenticate != null && authenticate.trim().toUpperCase(Locale.ROOT).startsWith("BASIC")) {
- HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter
- .getCredentials(request, defaultCredentials, HttpAuthenticationFilter.Type.BASIC);
-
- if (credentials == null) {
- if (response.hasEntity()) {
- AuthenticationUtil.discardInputAndClose(response.getEntityStream());
- }
- throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC());
- }
-
- return HttpAuthenticationFilter.repeatRequest(request, response, calculateAuthentication(credentials));
+ final List<String> authHeaders = response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE);
+ if (authHeaders == null || authHeaders.size() == 0 || authHeaders.stream()
+ .noneMatch(h -> h != null && h.toUpperCase(Locale.ROOT).startsWith("BASIC"))) {
+ return false;
}
- return false;
+
+ HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter
+ .getCredentials(request, defaultCredentials, HttpAuthenticationFilter.Type.BASIC);
+
+ if (credentials == null) {
+ if (response.hasEntity()) {
+ AuthenticationUtil.discardInputAndClose(response.getEntityStream());
+ }
+ throw new ResponseAuthenticationException(null, LocalizationMessages.AUTHENTICATION_CREDENTIALS_MISSING_BASIC());
+ }
+
+ return HttpAuthenticationFilter.repeatRequest(request, response, calculateAuthentication(credentials));
}
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
index 79dc60e..67d4a63 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -20,12 +20,14 @@
import org.glassfish.jersey.client.ClientRequest;
import org.glassfish.jersey.http.HttpHeaders;
import org.glassfish.jersey.internal.PropertiesResolver;
+import org.glassfish.jersey.internal.guava.InetAddresses;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.UriBuilder;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
@@ -233,7 +235,9 @@
String host = uri.getHost();
try {
InetAddress ip = InetAddress.getByName(host);
- return UriBuilder.fromUri(uri).host(ip.getHostAddress()).build();
+ // ipv6 is expected in square brackets in UriBuilder#host()
+ final String hostAddress = ip instanceof Inet6Address ? '[' + ip.getHostAddress() + ']' : ip.getHostAddress();
+ return UriBuilder.fromUri(uri).host(hostAddress).build();
} catch (UnknownHostException e) {
return uri;
}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
index baf1c28..4bd2856 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SniConfigurator.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -60,7 +60,10 @@
static Optional<SniConfigurator> createWhenHostHeader(URI hostUri, String sniHost, boolean whenDiffer) {
final String trimmedHeader;
if (sniHost != null) {
- int index = sniHost.indexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ;
+ int index = sniHost.lastIndexOf(':'); // RFC 7230 Host = uri-host [ ":" port ] ;
+ if (sniHost.indexOf(']', index) != -1) {
+ index = -1; // beware of ipv6 [:1] without port
+ }
final String trimmedHeader0 = index != -1 ? sniHost.substring(0, index).trim() : sniHost.trim();
trimmedHeader = trimmedHeader0.isEmpty() ? sniHost : trimmedHeader0;
} else {
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
new file mode 100644
index 0000000..1bac330
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client;
+
+import jakarta.ws.rs.SeBootstrap;
+import jakarta.ws.rs.core.EntityPart;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.GenericEntity;
+import jakarta.ws.rs.core.Link;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.core.Variant;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.ReaderInterceptor;
+import jakarta.ws.rs.ext.ReaderInterceptorContext;
+import jakarta.ws.rs.ext.RuntimeDelegate;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AbortTest {
+ private static final String TEXT_CSV = "text/csv";
+ private static final String TEXT_HEADER = "text/header";
+ private static final String EXPECTED_CSV = "hello;goodbye\nsalutations;farewell";
+ private static final List<List<String>> CSV_LIST = Arrays.asList(
+ Arrays.asList("hello", "goodbye"),
+ Arrays.asList("salutations", "farewell")
+ );
+ private final String entity = "HI";
+ private final String header = "CUSTOM_HEADER";
+
+
+ @Test
+ void testAbortWithGenericEntity() {
+ Client client = ClientBuilder.newBuilder()
+ .register(AbortRequestFilter.class)
+ .register(CsvWriter.class)
+ .build();
+ String csvString = client.target("http://localhost:8080")
+ .request(TEXT_CSV)
+ .get(String.class);
+ assertEquals(EXPECTED_CSV, csvString);
+ client.close();
+ }
+
+ public static class AbortRequestFilter implements ClientRequestFilter {
+
+ @Override
+ public void filter(ClientRequestContext requestContext) {
+ requestContext.abortWith(Response.ok(new GenericEntity<List<List<String>>>(CSV_LIST) {
+ }).type(TEXT_CSV).build());
+ }
+ }
+
+ @Produces(TEXT_CSV)
+ public static class CsvWriter implements MessageBodyWriter<List<List<String>>> {
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return List.class.isAssignableFrom(type) && genericType instanceof ParameterizedType
+ && ((ParameterizedType) genericType).getActualTypeArguments()[0] instanceof ParameterizedType
+ && String.class.equals(((ParameterizedType) ((ParameterizedType) genericType).getActualTypeArguments()[0])
+ .getActualTypeArguments()[0]);
+ }
+
+ @Override
+ public void writeTo(List<List<String>> csvList, Class<?> type, Type genericType, Annotation[] annotations,
+ MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
+ throws IOException, WebApplicationException {
+ List<String> rows = new ArrayList<>();
+ for (List<String> row : csvList) {
+ rows.add(String.join(";", row));
+ }
+ String csv = String.join("\n", rows);
+
+ entityStream.write(csv.getBytes(StandardCharsets.UTF_8));
+ entityStream.flush();
+ }
+ }
+
+ @Test
+ void testAbortWithMBWWritingHeaders() {
+ try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response.ok(entity, TEXT_HEADER).build());
+ }
+ }).register(new MessageBodyWriter<String>() {
+
+ @Override
+ public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return mediaType.toString().equals(TEXT_HEADER);
+ }
+
+ @Override
+ public void writeTo(String s, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+ MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
+ WebApplicationException {
+ httpHeaders.add(header, entity);
+ entityStream.write(s.getBytes());
+ }
+ }, Priorities.USER - 1).target("http://localhost:8080").request().get()) {
+ Assertions.assertEquals(entity, response.readEntity(String.class));
+ Assertions.assertEquals(entity, response.getHeaderString(header));
+ }
+ }
+
+ @Test
+ void testInterceptorHeaderAdd() {
+ final String header2 = "CUSTOM_HEADER_2";
+
+ try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response.ok().entity(entity).build());
+ }
+ }).register(new ReaderInterceptor() {
+ @Override
+ public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
+ MultivaluedMap<String, String> headers = context.getHeaders();
+ headers.put(header, Collections.singletonList(entity));
+ headers.add(header2, entity);
+ return context.proceed();
+ }
+ })
+ .target("http://localhost:8080").request().get()) {
+ Assertions.assertEquals(entity, response.readEntity(String.class));
+ Assertions.assertEquals(entity, response.getHeaderString(header));
+ Assertions.assertEquals(entity, response.getHeaderString(header2));
+ }
+ }
+
+ @Test
+ void testInterceptorHeaderIterate() {
+ final AtomicReference<String> originalHeader = new AtomicReference<>();
+
+ try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response.ok().header(header, header).entity(entity).build());
+ }
+ }).register(new ReaderInterceptor() {
+ @Override
+ public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
+ MultivaluedMap<String, String> headers = context.getHeaders();
+ Iterator<Map.Entry<String, List<String>>> it = headers.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, List<String>> next = it.next();
+ if (header.equals(next.getKey())) {
+ originalHeader.set(next.setValue(Collections.singletonList(entity)).get(0));
+ }
+ }
+ return context.proceed();
+ }
+ })
+ .target("http://localhost:8080").request().get()) {
+ Assertions.assertEquals(entity, response.readEntity(String.class));
+ Assertions.assertEquals(entity, response.getHeaderString(header));
+ Assertions.assertEquals(header, originalHeader.get());
+ }
+ }
+
+ @Test
+ void testNullHeader() {
+ final AtomicReference<String> originalHeader = new AtomicReference<>();
+ RuntimeDelegate.setInstance(new StringHeaderRuntimeDelegate(RuntimeDelegate.getInstance()));
+ try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response.ok()
+ .header(header, new StringHeader())
+ .entity(entity).build());
+ }
+ }).register(new ClientResponseFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
+ throws IOException {
+ originalHeader.set(responseContext.getHeaderString(header));
+ }
+ })
+ .target("http://localhost:8080").request().get()) {
+ Assertions.assertEquals(entity, response.readEntity(String.class));
+ Assertions.assertEquals("", originalHeader.get());
+ }
+ }
+
+ private static class StringHeader extends AtomicReference<String> {
+
+ }
+
+ private static class StringHeaderDelegate implements RuntimeDelegate.HeaderDelegate<StringHeader> {
+ @Override
+ public StringHeader fromString(String value) {
+ StringHeader stringHeader = new StringHeader();
+ stringHeader.set(value);
+ return stringHeader;
+ }
+
+ @Override
+ public String toString(StringHeader value) {
+ //on purpose
+ return null;
+ }
+ }
+
+ private static class StringHeaderRuntimeDelegate extends RuntimeDelegate {
+ private final RuntimeDelegate original;
+
+ private StringHeaderRuntimeDelegate(RuntimeDelegate original) {
+ this.original = original;
+ }
+
+ @Override
+ public UriBuilder createUriBuilder() {
+ return original.createUriBuilder();
+ }
+
+ @Override
+ public Response.ResponseBuilder createResponseBuilder() {
+ return original.createResponseBuilder();
+ }
+
+ @Override
+ public Variant.VariantListBuilder createVariantListBuilder() {
+ return original.createVariantListBuilder();
+ }
+
+ @Override
+ public <T> T createEndpoint(Application application, Class<T> endpointType)
+ throws IllegalArgumentException, UnsupportedOperationException {
+ return original.createEndpoint(application, endpointType);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> HeaderDelegate<T> createHeaderDelegate(Class<T> type) throws IllegalArgumentException {
+ if (StringHeader.class.equals(type)) {
+ return (HeaderDelegate<T>) new StringHeaderDelegate();
+ }
+ return original.createHeaderDelegate(type);
+ }
+
+ @Override
+ public Link.Builder createLinkBuilder() {
+ return original.createLinkBuilder();
+ }
+
+ @Override
+ public SeBootstrap.Configuration.Builder createConfigurationBuilder() {
+ return original.createConfigurationBuilder();
+ }
+
+ @Override
+ public CompletionStage<SeBootstrap.Instance> bootstrap(Application application, SeBootstrap.Configuration configuration) {
+ return original.bootstrap(application, configuration);
+ }
+
+ @Override
+ public CompletionStage<SeBootstrap.Instance> bootstrap(Class<? extends Application> clazz,
+ SeBootstrap.Configuration configuration) {
+ return original.bootstrap(clazz, configuration);
+ }
+
+ @Override
+ public EntityPart.Builder createEntityPartBuilder(String partName) throws IllegalArgumentException {
+ return original.createEntityPartBuilder(partName);
+ }
+ }
+
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java
new file mode 100644
index 0000000..ad5b721
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/authentication/BasicAuthenticatorTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.client.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class BasicAuthenticatorTest {
+
+ @Test
+ void filterResponseAndAuthenticateNoAuthHeadersTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ when(response.getHeaders()).thenReturn(mock(MultivaluedMap.class));
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateAuthHeaderNotBasicTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Collections.singletonList("Digest realm=\"test\""));
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateEmptyListTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Collections.emptyList());
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateNullListTest() {
+ final BasicAuthenticator authenticator
+ = new BasicAuthenticator(new HttpAuthenticationFilter.Credentials("foo", "bar"));
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(null);
+
+ assertFalse(authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+ @Test
+ void filterResponseAndAuthenticateMissingCredentialsMultipleAuthRealmsTest() {
+ final String[] authHeaders = new String[] {
+ "Digest realm=\"test\"",
+ "Basic realm=\"test\""
+ };
+ final BasicAuthenticator authenticator = new BasicAuthenticator(null);
+ final ClientRequestContext request = mock(ClientRequestContext.class);
+ final ClientResponseContext response = mock(ClientResponseContext.class);
+
+ final MultivaluedMap<String, String> headers = mock(MultivaluedMap.class);
+ when(response.getHeaders()).thenReturn(headers);
+ when(headers.get(HttpHeaders.WWW_AUTHENTICATE)).thenReturn(Arrays.asList(authHeaders));
+ when(response.hasEntity()).thenReturn(false);
+
+ assertThrows(ResponseAuthenticationException.class,
+ () -> authenticator.filterResponseAndAuthenticate(request, response));
+ }
+
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
index 06dcdec..e7a61d0 100644
--- a/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
+++ b/core-client/src/test/java/org/glassfish/jersey/client/innate/http/SSLParamConfiguratorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -27,9 +27,13 @@
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.Response;
+import java.net.ConnectException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
@@ -155,4 +159,56 @@
MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is("yyy.com"));
}
+
+ @Test
+ public void testIPv6Header() {
+ final String HOST_HEADER_IPv6 = "[172:30::333b]";
+ final URI uri = URI.create("http://[172:30::333a]:8080/api/demo/v1");
+ final JerseyClient client = (JerseyClient) ClientBuilder.newClient();
+ Map<String, List<Object>> httpHeaders = new MultivaluedHashMap<>();
+ httpHeaders.put(HttpHeaders.HOST, Collections.singletonList(HOST_HEADER_IPv6 + ":8080"));
+ SSLParamConfigurator configurator = SSLParamConfigurator.builder()
+ .uri(uri)
+ .headers(httpHeaders)
+ .configuration(client.getConfiguration())
+ .build();
+ MatcherAssert.assertThat(configurator.isSNIRequired(), Matchers.is(true));
+ MatcherAssert.assertThat(configurator.getSNIHostName(), Matchers.is(HOST_HEADER_IPv6));
+ URI expected = URI.create("http://" + HOST_HEADER_IPv6 + ":8080/api/demo/v1");
+ MatcherAssert.assertThat(configurator.getSNIUri(), Matchers.is(expected));
+ MatcherAssert.assertThat(configurator.toIPRequestUri().toString(),
+ Matchers.is(uri.toString().replace("::", ":0:0:0:0:0:")));
+ }
+
+ @Test
+ public void testIpv6Request() {
+ Client client = ClientBuilder.newClient();
+ String u = "http://[::1]:8080";
+ try {
+ client.target(u)
+ .request()
+ .header(HttpHeaders.HOST, "[172:30::333b]:8080")
+ .get();
+ } catch (ProcessingException pe) {
+ if (!ConnectException.class.isInstance(pe.getCause())) {
+ throw pe;
+ }
+ }
+ }
+
+ @Test
+ public void testIpv6RequestNoPort() {
+ Client client = ClientBuilder.newClient();
+ String u = "http://[::1]";
+ try {
+ client.target(u)
+ .request()
+ .header(HttpHeaders.HOST, "[172:30::333b]")
+ .get();
+ } catch (ProcessingException pe) {
+ if (!ConnectException.class.isInstance(pe.getCause())) {
+ throw pe;
+ }
+ }
+ }
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
index 4765056..75b1987 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -129,60 +129,107 @@
* @return transformed map view.
*/
public static <K, V, O> Map<K, V> mapView(Map<K, O> originalMap, Function<O, V> valuesTransformer) {
- return new AbstractMap<K, V>() {
- @Override
- public Set<Entry<K, V>> entrySet() {
- return new AbstractSet<Entry<K, V>>() {
+ return new KVOMap<K, V, O>(originalMap, valuesTransformer);
+ }
+ private static class KVOMap<K, V, O> extends AbstractMap<K, V> {
+ protected final Map<K, O> originalMap;
+ protected final Function<O, V> valuesTransformer;
- Set<Entry<K, O>> originalSet = originalMap.entrySet();
- Iterator<Entry<K, O>> original = originalSet.iterator();
+ private KVOMap(Map<K, O> originalMap, Function<O, V> valuesTransformer) {
+ this.originalMap = originalMap;
+ this.valuesTransformer = valuesTransformer;
+ }
- @Override
- public Iterator<Entry<K, V>> iterator() {
- return new Iterator<Entry<K, V>>() {
- @Override
- public boolean hasNext() {
- return original.hasNext();
- }
+ @Override
+ public Set<Entry<K, V>> entrySet() {
+ return new AbstractSet<Entry<K, V>>() {
- @Override
- public Entry<K, V> next() {
+ Set<Entry<K, O>> originalSet = originalMap.entrySet();
+ Iterator<Entry<K, O>> original = originalSet.iterator();
- Entry<K, O> next = original.next();
+ @Override
+ public Iterator<Entry<K, V>> iterator() {
+ return new Iterator<Entry<K, V>>() {
+ @Override
+ public boolean hasNext() {
+ return original.hasNext();
+ }
- return new Entry<K, V>() {
- @Override
- public K getKey() {
- return next.getKey();
- }
+ @Override
+ public Entry<K, V> next() {
- @Override
- public V getValue() {
- return valuesTransformer.apply(next.getValue());
- }
+ Entry<K, O> next = original.next();
- @Override
- public V setValue(V value) {
- throw new UnsupportedOperationException("Not supported.");
- }
- };
- }
+ return new Entry<K, V>() {
+ @Override
+ public K getKey() {
+ return next.getKey();
+ }
- @Override
- public void remove() {
- original.remove();
- }
- };
- }
+ @Override
+ public V getValue() {
+ return valuesTransformer.apply(next.getValue());
+ }
- @Override
- public int size() {
- return originalSet.size();
- }
- };
- }
- };
+ @Override
+ public V setValue(V value) {
+ return KVOMap.this.setValue(next, value);
+ }
+ };
+ }
+
+ @Override
+ public void remove() {
+ original.remove();
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return originalSet.size();
+ }
+ };
+ }
+
+ protected V setValue(Map.Entry<K, O> entry, V value) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+ }
+
+ /**
+ * Create a {@link Map} view, which transforms the values of provided original map.
+ * <p>
+ *
+ * @param originalMap provided map.
+ * @param valuesTransformer values transformer.
+ * @return transformed map view.
+ */
+ public static Map<String, List<String>> mapObjectToStringView(Map<String, List<Object>> originalMap,
+ Function<List<Object>, List<String>> valuesTransformer) {
+ return new ObjectToStringMap(originalMap, valuesTransformer);
+ }
+
+ private static class ObjectToStringMap extends KVOMap<String, List<String>, List<Object>> {
+
+ private ObjectToStringMap(Map<String, List<Object>> originalMap, Function<List<Object>, List<String>> valuesTransformer) {
+ super(originalMap, valuesTransformer);
+ }
+
+ @Override
+ protected List<String> setValue(Entry<String, List<Object>> entry, List<String> value) {
+ @SuppressWarnings("unchecked")
+ final List<Object> old = entry.setValue((List<Object>) (List<?>) value);
+ return valuesTransformer.apply(old);
+ }
+
+ @Override
+ public List<String> put(String key, List<String> value) {
+ @SuppressWarnings("unchecked")
+ final List<Object> old = originalMap.put(key, (List<Object>) (List<?>) value);
+ return valuesTransformer.apply(old);
+ }
}
/**
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
index 14a008d..958a1bc 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/CookiesParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -86,20 +86,29 @@
}
/**
- * Check if a cookie with identical name had been parsed.
- * If yes, the one with the longest string will be kept
+ * Check if a cookie with similar names had been parsed.
+ * If yes, the one with the longest path will be kept
+ * For similar paths the newest is stored
* @param cookies : Map of cookies
* @param cookie : Cookie to be checked
*/
private static void checkSimilarCookieName(Map<String, Cookie> cookies, MutableCookie cookie) {
- if (cookie != null) {
- if (cookies.containsKey(cookie.name)){
- if (cookie.value.length() > cookies.get(cookie.name).getValue().length()){
- cookies.put(cookie.name, cookie.getImmutableCookie());
- }
- } else {
- cookies.put(cookie.name, cookie.getImmutableCookie());
- }
+ if (cookie == null) {
+ return;
+ }
+
+ boolean alreadyPresent = cookies.containsKey(cookie.name);
+ boolean recordCookie = !alreadyPresent;
+
+ if (alreadyPresent) {
+ final String newPath = cookie.path == null ? "" : cookie.path;
+ final String existingPath = cookies.get(cookie.name).getPath() == null ? ""
+ : cookies.get(cookie.name).getPath();
+ recordCookie = (newPath.length() >= existingPath.length());
+ }
+
+ if (recordCookie) {
+ cookies.put(cookie.name, cookie.getImmutableCookie());
}
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
index aeea686..c60386b 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -210,7 +210,7 @@
}
return new AbstractMultivaluedMap<String, String>(
- Views.mapView(headers, input -> HeaderUtils.asStringList(input, rd))
+ Views.mapObjectToStringView(headers, input -> HeaderUtils.asStringList(input, rd))
) {
};
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index 29cb6e0..46446a4 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -157,8 +157,22 @@
* as required by JAX-RS specification on the server side.
*/
public InboundMessageContext(Configuration configuration, boolean translateNce) {
+ this(configuration, HeaderUtils.createInbound(), translateNce);
+ }
+
+ /**
+ * Create new inbound message context.
+ *
+ * @param configuration the related client/server side {@link Configuration}. If {@code null},
+ * the default behaviour is expected.
+ * @param httpHeaders the http headers map.
+ * @param translateNce if {@code true}, the {@link javax.ws.rs.core.NoContentException} thrown by a
+ * selected message body reader will be translated into a {@link javax.ws.rs.BadRequestException}
+ * as required by JAX-RS specification on the server side.
+ */
+ public InboundMessageContext(Configuration configuration, MultivaluedMap<String, String> httpHeaders, boolean translateNce) {
super(configuration);
- this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createInbound());
+ this.headers = new GuardianStringKeyMultivaluedMap<>(httpHeaders);
this.entityContent = new EntityContent();
this.translateNce = translateNce;
this.configuration = configuration;
@@ -302,7 +316,11 @@
}
final Iterator<String> valuesIterator = values.iterator();
- StringBuilder buffer = new StringBuilder(valuesIterator.next());
+ String next = valuesIterator.next();
+ if (next == null) {
+ next = "";
+ }
+ StringBuilder buffer = new StringBuilder(next);
while (valuesIterator.hasNext()) {
buffer.append(',').append(valuesIterator.next());
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index efa8899..953c27f 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -134,7 +134,7 @@
*/
@Deprecated
public OutboundMessageContext() {
- this ((Configuration) null);
+ this((Configuration) null);
}
/**
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
index ae8e7d8..dd0fc8e 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -184,11 +184,23 @@
}
return sb.toString();
}
+
/**
- * The maximum size of array to allocate.
+ * Read/convert stream to the byte array.
+ *
+ * @param in stream to be converted to the byte array
+ * @return the byte array
+ * @throws IOException if there is an error reading from the stream
+ * @since 2.47
+ */
+ public static byte[] readFromAsBytes(InputStream in) throws IOException {
+ return readAllBytes(in);
+ }
+ /**
+ * The maximum size of an array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
- * OutOfMemoryError: Requested array size exceeds VM limit
+ * OutOfMemoryError: Requested array size exceeds the VM limit
*/
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
index f5d846a..e027bdc 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
@@ -195,7 +195,7 @@
this.b = classFileBuffer;
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
- if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V24) {
+ if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V25) {
throw new IllegalArgumentException(
"Unsupported class file major version " + readShort(classFileOffset + 6));
}
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
index e2f2c92..29eac8f 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodVisitor.java
@@ -596,7 +596,7 @@
* Visits a LOOKUPSWITCH instruction.
*
* @param dflt beginning of the default handler block.
- * @param keys the values of the keys.
+ * @param keys the values of the keys. Keys must be sorted in increasing order.
* @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the
* handler block for the {@code keys[i]} key.
*/
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
index eeb3df7..8a6ca40 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
@@ -290,6 +290,7 @@
int V22 = 0 << 16 | 66;
int V23 = 0 << 16 | 67;
int V24 = 0 << 16 | 68;
+ int V25 = 0 << 16 | 69;
/**
* Version flag indicating that the class is using 'preview' features.
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
index 917d094..807252c 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -309,7 +309,7 @@
private static class ClassReaderWrapper {
private static final Logger LOGGER = Logger.getLogger(ClassReader.class.getName());
- private static final int WARN_VERSION = Opcodes.V24;
+ private static final int WARN_VERSION = Opcodes.V25;
private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096;
private final byte[] b;
diff --git a/core-server/src/main/resources/META-INF/NOTICE.markdown b/core-server/src/main/resources/META-INF/NOTICE.markdown
index 2016cb4..ab6fea0 100644
--- a/core-server/src/main/resources/META-INF/NOTICE.markdown
+++ b/core-server/src/main/resources/META-INF/NOTICE.markdown
@@ -36,7 +36,7 @@
* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.
* Copyright 2010-2013 Coda Hale and Yammer, Inc.
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/examples/NOTICE.md b/examples/NOTICE.md
index ed616c2..d1be3d9 100644
--- a/examples/NOTICE.md
+++ b/examples/NOTICE.md
@@ -96,7 +96,7 @@
* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS
* Copyright: Eric Rowell
-org.objectweb.asm Version 9.7.1
+org.objectweb.asm Version 9.8
* License: Modified BSD (https://asm.ow2.io/license.html)
* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.
diff --git a/examples/expect-100-continue-netty-client/README.MD b/examples/expect-100-continue-netty-client/README.MD
new file mode 100644
index 0000000..5749184
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/README.MD
@@ -0,0 +1,62 @@
+[//]: # " Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved. "
+[//]: # " "
+[//]: # " This program and the accompanying materials are made available under the "
+[//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at "
+[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. "
+[//]: # " "
+[//]: # " SPDX-License-Identifier: BSD-3-Clause "
+
+jersey-example-expect-100-continue-netty-connector
+==========================================================
+
+This example demonstrates how to register and run Jersey Netty connector with Expect:100-continue feature on.
+It also provides custom low-level Socket server to demonstrate how is request is captured and processed.
+
+Contents
+--------
+
+The server and client are operating on requests level, without exposing any Resources. Client only sends request in
+form
+```json
+{"message":"Hello from java client"}
+```
+
+Sample Response
+---------------
+Server in turn shows output which demonstrates Expect:100-continue presence and handling
+
+```shell
+==== DUMPING HEADERS ====
+expect, 100-continue
+transfer-encoding, chunked
+host, 127.0.0.1:3000
+content-type, application/json
+accept, application/json
+user-agent, jersey/2.47-snapshot (netty 4.1.112.final)
+==== HEADERS DUMPED =====
+==== DUMPING RESPONSE ====
+HTTP/1.1 100 Continue
+Connection: keep-alive
+
+
+==== RESPONSE DUMPED =====
+24
+{"message":"Hello from java client"}
+==== DUMPING RESPONSE ====
+HTTP/1.1 204 No Content
+Server: Socket Server v.0.0.1
+
+
+```
+
+Running the Example
+-------------------
+
+Run the example using provided ServerSocket container as follows:
+
+> mvn clean compile exec:java
+
+Run the example using client as follows:
+
+> mvn clean package exec:java -Pclient
+
diff --git a/examples/expect-100-continue-netty-client/pom.xml b/examples/expect-100-continue-netty-client/pom.xml
new file mode 100644
index 0000000..287a9b6
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Distribution License v. 1.0, which is available at
+ http://www.eclipse.org/org/documents/edl-v10.php.
+
+ SPDX-License-Identifier: BSD-3-Clause
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.glassfish.jersey.examples</groupId>
+ <artifactId>project</artifactId>
+ <version>3.1.99-SNAPSHOT</version>
+ </parent>
+
+
+
+ <artifactId>jersey-example-expect-100-continue-netty-client</artifactId>
+ <packaging>jar</packaging>
+ <name>jersey-example-expect-100-continue-netty-client</name>
+
+ <description>Jersey example for Expect: 100-continue header usage with netty connector.</description>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.connectors</groupId>
+ <artifactId>jersey-netty-connector</artifactId>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>${mainClass}</mainClass>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <inherited>true</inherited>
+ <configuration>
+ <showWarnings>false</showWarnings>
+ <fork>false</fork>
+ </configuration>
+ </plugin>
+
+ <!-- Run the application using "mvn jetty:run" -->
+
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>client</id>
+ <properties>
+ <mainClass>org.glassfish.jersey.examples.expect100continue.netty.connector.NettyClient</mainClass>
+ </properties>
+ </profile>
+ <profile>
+ <id>pre-release</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>xml-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+ <properties>
+ <mainClass>org.glassfish.jersey.examples.expect100continue.netty.connector.SocketServer</mainClass>
+ </properties>
+</project>
diff --git a/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java
new file mode 100644
index 0000000..fa27674
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/NettyClient.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.expect100continue.netty.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.http.Expect100ContinueFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class NettyClient {
+ public static void main(String[] args) throws InterruptedException {
+// enableLogging(Level.FINE);
+ test();
+ }
+
+ public static void test() throws InterruptedException {
+ ClientConfig defaultConfig = new ClientConfig();
+ defaultConfig.property(LoggingFeature.LOGGING_FEATURE_VERBOSITY_CLIENT, LoggingFeature.Verbosity.PAYLOAD_ANY);
+
+ //The issue can be produced only by using NettyConnectorProvider
+ defaultConfig.connectorProvider(new NettyConnectorProvider());
+
+ //with below two lines, enabled 100-continue feature
+ defaultConfig.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED);
+ defaultConfig.register(Expect100ContinueFeature.basic());
+
+ Client client = ClientBuilder.newClient(defaultConfig);
+ WebTarget webTarget = client.target("http://127.0.0.1:3000");
+ Invocation.Builder invocationBuilder = webTarget.request();
+ invocationBuilder.header("Accept", "application/json");
+
+ for (int i = 0; i < 5; i++) { //iterating few times here to demonstrate
+ // the 100-continue processing works on any iteration
+
+ System.out.println();
+ System.out.println("****************** Iteration #" + i + " ******************");
+
+ final Response response = invocationBuilder.post(generateSimpleEntity());
+
+ System.out.println("Response status = " + response.getStatus());
+ System.out.println("Response status 204 means No Content, so we do not expect body here");
+ System.out.println("**************************************************");
+ System.out.println();
+ }
+ System.out.println("Client connection should be closed manually with Ctrl-C");
+ }
+
+ private static Entity<String> generateSimpleEntity(){
+ return Entity.entity("{\"message\":\"Hello from java client\"}", MediaType.APPLICATION_JSON_TYPE);
+ }
+
+ private static void enableLogging(Level logLevel) {
+ Logger rootLogger = Logger.getLogger("");
+ rootLogger.setLevel(logLevel);
+ Logger nettyLog = Logger.getLogger("io.netty");
+ nettyLog.setLevel(logLevel);
+ Handler[] handlers = rootLogger.getHandlers();
+ for (final Handler handler : handlers) {
+ handler.setLevel(logLevel);
+ }
+ }
+}
diff --git a/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java
new file mode 100644
index 0000000..6fd5699
--- /dev/null
+++ b/examples/expect-100-continue-netty-client/src/main/java/org/glassfish/jersey/examples/expect100continue/netty/connector/SocketServer.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.expect100continue.netty.connector;
+
+import javax.net.ServerSocketFactory;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class SocketServer {
+ private static final String NO_CONTENT_HEADER = "HTTP/1.1 204 No Content";
+ private static final String OK_HEADER = "HTTP/1.1 200 OK";
+ private static final String EXPECT_HEADER = "HTTP/1.1 100 Continue";
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
+ private AtomicBoolean expect_processed = new AtomicBoolean(false);
+
+ private ServerSocket server;
+
+ private static final boolean debug = true;
+
+ private static final int port = 3000;
+
+ private volatile boolean stopped = false;
+
+ public static void main(String args[]) throws IOException {
+ new SocketServer(port).runServer();
+ }
+
+ SocketServer(int port) throws IOException {
+ final ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
+ server = socketFactory.createServerSocket(port);
+ }
+
+ void stop() {
+ stopped = true;
+ try {
+ server.close();
+ executorService.shutdown();
+ while (!executorService.isTerminated()) {
+ executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
+ }
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void runServer() {
+
+ executorService.execute(() -> {
+ try {
+ dumpServerReadMe();
+ while (!stopped) {
+ final Socket socket = server.accept();
+ executorService.submit(() -> processRequest(socket));
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private void processRequest(final Socket request) {
+
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(request.getOutputStream()))) {
+
+
+ while (!stopped) {
+ final Map<String, String> headers = mapHeaders(reader);
+
+ if (headers.isEmpty()) {
+ continue;
+ }
+ if (debug) {
+ dumpHeaders(headers);
+ }
+
+ boolean failed = processExpect100Continue(headers, writer);
+
+ if (failed) {
+ continue;
+ }
+
+ final String http_header = expect_processed.get() ? NO_CONTENT_HEADER : OK_HEADER;
+ boolean read = readBody(reader, headers);
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+ addNewLineToResponse(responseBuffer);
+ addServerHeaderToResponse(responseBuffer);
+// addToResponse("Content-Length: 0", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ if (debug) {
+ dumpResponse(responseBuffer);
+ }
+
+ writer.write(responseBuffer.toString());
+
+ writer.flush();
+ if (read) {
+ break;
+ }
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ request.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addNewLineToResponse(StringBuffer responseBuffer) {
+ addToResponse("\r\n", responseBuffer);
+ }
+
+ private void addToResponse(String toBeAdded, StringBuffer responseBuffer) {
+ responseBuffer.append(toBeAdded);
+ }
+
+ private void addServerHeaderToResponse(StringBuffer responseBuffer) {
+ addToResponse("Server: Example Socket Server v.0.0.1", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ }
+
+ private boolean processExpect100Continue(Map<String, String> headers, BufferedWriter writer) throws IOException {
+ String http_header = EXPECT_HEADER;
+ boolean failed = false;
+ final String continueHeader = headers.remove("expect");
+
+ if (continueHeader != null && continueHeader.contains("100-continue")) {
+
+ expect_processed.set(http_header.equals(EXPECT_HEADER));
+
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+
+ addNewLineToResponse(responseBuffer);
+ addToResponse("Connection: keep-alive", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ if (debug) {
+ dumpResponse(responseBuffer);
+ }
+
+ writer.write(responseBuffer.toString());
+ writer.flush();
+ }
+ return failed;
+ }
+
+ private Map<String, String> mapHeaders(BufferedReader reader) throws IOException {
+ String line;
+ final Map<String, String> headers = new HashMap<>();
+
+
+ if (!reader.ready()) {
+ return headers;
+ }
+
+ while ((line = reader.readLine()) != null && !line.isEmpty()) {
+
+
+ int pos = line.indexOf(':');
+ if (pos > -1) {
+ headers.put(
+ line.substring(0, pos).toLowerCase(Locale.ROOT),
+ line.substring(pos + 2).toLowerCase(Locale.ROOT).trim());
+ }
+ }
+
+ return headers;
+ }
+
+ private boolean readBody(BufferedReader reader, Map<String, String> headers) throws IOException {
+ if (headers.containsKey("content-length")) {
+ int contentLength = Integer.valueOf(headers.get("content-length"));
+ int actualLength = 0, readingByte = 0;
+ int[] buffer = new int[contentLength];
+ while (actualLength < contentLength && (readingByte = reader.read()) != -1) {
+ buffer[actualLength++] = readingByte;
+ }
+ if (debug) {
+ System.out.println("Reading " + actualLength + " of " + contentLength + " bytes/chars");
+ }
+ return (actualLength == contentLength);
+ } else if (headers.containsKey("transfer-encoding")) {
+ String line;
+ while ((line = reader.readLine()) != null && !line.equals("0")) {
+ if (debug) {
+ System.out.println(line);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void dumpHeaders(Map<String, String> headers) {
+ System.out.println("==== DUMPING HEADERS ====");
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ System.out.println(entry.getKey() + ", " + entry.getValue());
+ }
+ System.out.println("==== HEADERS DUMPED =====");
+ }
+
+ private void dumpResponse(StringBuffer responseBuffer) {
+ System.out.println("==== DUMPING RESPONSE ====");
+ System.out.println(responseBuffer);
+ System.out.println("==== RESPONSE DUMPED =====");
+ }
+
+ private void dumpServerReadMe() {
+ System.out.println("==================================Server is running========================================");
+ System.out.println("= *** =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= You can send requests to it either using Netty Client or curl or any other http tool. =");
+ System.out.println("= Try to modify it to see how Expect: 100-continue header works. =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= stop server by Ctrl-c =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= Run server using maven: =");
+ System.out.println("= mvn clean package exec:java =");
+ System.out.println("= Run client using maven: =");
+ System.out.println("= mvn clean package exec:java -Pclient =");
+ System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
+ System.out.println("= *** =");
+ System.out.println("===========================================================================================");
+ }
+
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index 540858b..72695a3 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -67,6 +67,7 @@
<module>entity-filtering-security</module>
<module>extended-wadl-webapp</module>
<module>exception-mapping</module>
+ <module>expect-100-continue-netty-client</module>
<!--<module>feed-combiner-java8-webapp</module>-->
<module>freemarker-webapp</module>
<!--<module>flight-mgmt-webapp</module>-->
diff --git a/examples/servlet3-webapp/pom.xml b/examples/servlet3-webapp/pom.xml
index 51b85c2..8693516 100644
--- a/examples/servlet3-webapp/pom.xml
+++ b/examples/servlet3-webapp/pom.xml
@@ -112,15 +112,6 @@
<profiles>
<profile>
- <id>jdk8_tests</id>
- <activation>
- <jdk>1.8</jdk>
- </activation>
- <properties>
- <junit5.version>${junit5.jdk8.version}</junit5.version>
- </properties>
- </profile>
- <profile>
<id>pre-release</id>
<build>
<plugins>
diff --git a/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java b/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
index ef96d16..e9437c4 100644
--- a/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
+++ b/inject/hk2/src/main/java/org/glassfish/jersey/inject/hk2/Hk2RequestScope.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,8 +16,9 @@
package org.glassfish.jersey.inject.hk2;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
@@ -63,7 +64,7 @@
private final AtomicInteger referenceCounter;
private Instance() {
- this.store = new HashMap<>();
+ this.store = new LinkedHashMap<>();
this.referenceCounter = new AtomicInteger(1);
}
@@ -140,7 +141,9 @@
public void release() {
if (referenceCounter.decrementAndGet() < 1) {
try {
- new HashSet<>(store.keySet()).forEach(this::remove);
+ ArrayList<ForeignDescriptor> reverse = new ArrayList<>(store.keySet());
+ Collections.reverse(reverse);
+ reverse.forEach(this::remove);
} finally {
logger.debugLog("Released scope instance {0}", this);
}
diff --git a/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java b/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
index ee9f56d..8729e4e 100644
--- a/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
+++ b/media/sse/src/main/java/org/glassfish/jersey/media/sse/internal/EventProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -400,7 +400,7 @@
*/
public Builder reconnectDelay(long reconnectDelay, TimeUnit unit) {
this.reconnectDelay = reconnectDelay;
- this.reconnectUnit = reconnectUnit;
+ this.reconnectUnit = unit;
return this;
}
diff --git a/pom.xml b/pom.xml
index d5a6861..b3ed8d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1311,6 +1311,26 @@
</plugins>
</build>
</profile>
+ <profile>
+ <id>scan</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.owasp</groupId>
+ <artifactId>dependency-check-maven</artifactId>
+ <version>12.1.1</version>
+ <!-- mvn -Pscan org.owasp:dependency-check-maven:check -->
+ <executions>
+ <execution>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
</profiles>
<reporting>
@@ -1524,7 +1544,7 @@
<dependency>
<groupId>org.glassfish.hk2</groupId>
<artifactId>osgi-resource-locator</artifactId>
- <version>1.0.3</version>
+ <version>1.0.3</version> <!-- JDK 17 needed for 1.0.4 -->
</dependency>
<dependency>
<groupId>org.glassfish.main.hk2</groupId>
@@ -2071,16 +2091,16 @@
<!-- Versions of Maven plugins -->
<antrun.mvn.plugin.version>3.1.0</antrun.mvn.plugin.version>
- <mvn.ant.version>1.10.14</mvn.ant.version>
+ <mvn.ant.version>1.10.15</mvn.ant.version>
<assembly.mvn.plugin.version>3.7.1</assembly.mvn.plugin.version>
- <clean.mvn.plugin.version>3.4.0</clean.mvn.plugin.version>
+ <clean.mvn.plugin.version>3.4.1</clean.mvn.plugin.version>
<enforcer.mvn.plugin.version>3.5.0</enforcer.mvn.plugin.version>
- <exec.mvn.plugin.version>3.4.1</exec.mvn.plugin.version>
+ <exec.mvn.plugin.version>3.5.0</exec.mvn.plugin.version>
<buildhelper.mvn.plugin.version>3.6.0</buildhelper.mvn.plugin.version>
- <buildnumber.mvn.plugin.version>3.2.0</buildnumber.mvn.plugin.version>
- <checkstyle.mvn.plugin.version>3.4.0</checkstyle.mvn.plugin.version>
- <checkstyle.version>10.17.0</checkstyle.version>
- <compiler.mvn.plugin.version>3.13.0</compiler.mvn.plugin.version>
+ <buildnumber.mvn.plugin.version>3.2.1</buildnumber.mvn.plugin.version>
+ <checkstyle.mvn.plugin.version>3.6.0</checkstyle.mvn.plugin.version>
+ <checkstyle.version>10.21.4</checkstyle.version>
+ <compiler.mvn.plugin.version>3.14.0</compiler.mvn.plugin.version>
<!--
Special version of the compiler plugin just for the jersey-common. All versions above
generate too much for OSGi manifest.mf imports (awt etc). The version 3.11.0 however
@@ -2089,24 +2109,24 @@
-->
<compiler.common.mvn.plugin.version>3.9.0</compiler.common.mvn.plugin.version>
<cyclonedx.mvn.plugin.version>2.8.1</cyclonedx.mvn.plugin.version>
- <dependency.mvn.plugin.version>3.7.1</dependency.mvn.plugin.version>
- <deploy.mvn.plugin.version>3.1.2</deploy.mvn.plugin.version>
+ <dependency.mvn.plugin.version>3.8.1</dependency.mvn.plugin.version>
+ <deploy.mvn.plugin.version>3.1.4</deploy.mvn.plugin.version>
<ear.mvn.plugin.version>3.3.0</ear.mvn.plugin.version>
- <failsafe.mvn.plugin.version>3.3.1</failsafe.mvn.plugin.version>
+ <failsafe.mvn.plugin.version>3.5.3</failsafe.mvn.plugin.version>
<felix.mvn.plugin.version>5.1.9</felix.mvn.plugin.version>
<findbugs.mvn.plugin.version>3.0.5</findbugs.mvn.plugin.version>
<gfembedded.mvn.plugin.version>5.1</gfembedded.mvn.plugin.version>
- <install.mvn.plugin.version>3.1.2</install.mvn.plugin.version>
+ <install.mvn.plugin.version>3.1.4</install.mvn.plugin.version>
<istack.mvn.plugin.version>4.2.0</istack.mvn.plugin.version>
<jar.mvn.plugin.version>3.4.2</jar.mvn.plugin.version>
- <javadoc.mvn.plugin.version>3.8.0</javadoc.mvn.plugin.version>
- <jxr.mvn.plugin.version>3.4.0</jxr.mvn.plugin.version>
+ <javadoc.mvn.plugin.version>3.11.2</javadoc.mvn.plugin.version>
+ <jxr.mvn.plugin.version>3.6.0</jxr.mvn.plugin.version>
<paxexam.mvn.plugin.version>1.2.4</paxexam.mvn.plugin.version>
- <project.info.reports.mvn.plugin.version>3.6.2</project.info.reports.mvn.plugin.version>
+ <project.info.reports.mvn.plugin.version>3.9.0</project.info.reports.mvn.plugin.version>
<resources.mvn.plugin.version>3.3.1</resources.mvn.plugin.version>
<shade.mvn.plugin.version>3.6.0</shade.mvn.plugin.version>
<source.mvn.plugin.version>3.3.1</source.mvn.plugin.version>
- <surefire.mvn.plugin.version>3.5.2</surefire.mvn.plugin.version>
+ <surefire.mvn.plugin.version>3.5.3</surefire.mvn.plugin.version>
<war.mvn.plugin.version>3.4.0</war.mvn.plugin.version>
<wiremock.mvn.plugin.version>2.11.0</wiremock.mvn.plugin.version>
<xml.mvn.plugin.version>1.1.0</xml.mvn.plugin.version>
@@ -2119,45 +2139,44 @@
<arquillian.weld.version>3.0.1.Final</arquillian.weld.version> <!-- 3.0.2.Final fails microprofile TCK tests -->
<!-- asm is now source integrated - keeping this property to see the version -->
<!-- see core-server/src/main/java/jersey/repackaged/asm/.. -->
- <asm.version>9.7.1</asm.version>
+ <asm.version>9.8</asm.version>
<!--required for spring (ext) modules integration -->
<aspectj.weaver.version>1.9.22.1</aspectj.weaver.version>
<!-- <bnd.plugin.version>2.3.6</bnd.plugin.version>-->
<bouncycastle.version>1.70</bouncycastle.version>
- <commons.io.version>2.16.1</commons.io.version>
- <commons.codec.version>1.16.1</commons.codec.version>
+ <commons.codec.version>1.18.0</commons.codec.version>
<!-- <commons-lang3.version>3.3.2</commons-lang3.version>-->
- <commons.logging.version>1.3.4</commons.logging.version>
+ <commons.logging.version>1.3.5</commons.logging.version>
<fasterxml.classmate.version>1.7.0</fasterxml.classmate.version>
<felix.eventadmin.version>1.6.4</felix.eventadmin.version>
<felix.framework.security.version>2.8.4</felix.framework.security.version>
<felix.framework.version>7.0.5</felix.framework.version>
<findbugs.glassfish.version>1.7</findbugs.glassfish.version>
<freemarker.version>2.3.33</freemarker.version>
- <gae.version>2.0.29</gae.version>
- <groovy.version>5.0.0-alpha-11</groovy.version>
- <gson.version>2.11.0</gson.version>
+ <gae.version>2.0.36</gae.version>
+ <groovy.version>5.0.0-alpha-12</groovy.version>
+ <gson.version>2.13.1</gson.version>
<!--versions, extracted here due to maven-enforcer-plugin -->
<!-- <commons.codec.version>1.15</commons.codec.version>-->
<com.uber.jaeger.version>0.27.0</com.uber.jaeger.version>
- <org.codehaus.gmavenplus.version>3.0.2</org.codehaus.gmavenplus.version>
+ <org.codehaus.gmavenplus.version>4.2.0</org.codehaus.gmavenplus.version>
<!-- end of versions extracted here due to maven-enforcer-plugin -->
<!-- micrometer -->
- <micrometer.version>1.12.4</micrometer.version>
+ <micrometer.version>1.15.0</micrometer.version>
<micrometer-tracing.version>1.0.12</micrometer-tracing.version>
<!-- microprofile -->
<microprofile.config.version>3.0.3</microprofile.config.version>
<microprofile.rest.client3.version>3.0.1</microprofile.rest.client3.version>
<microprofile.rest.client.version>4.0</microprofile.rest.client.version>
- <helidon.config.version>3.2.6</helidon.config.version>
- <helidon.connector.version>3.2.8</helidon.connector.version>
- <helidon.config.11.version>1.4.14</helidon.config.11.version> <!-- JDK 11- support -->
+ <helidon.config.version>3.2.12</helidon.config.version>
+ <helidon.connector.version>3.2.12</helidon.connector.version>
+ <helidon.config.11.version>1.4.15</helidon.config.11.version> <!-- JDK 11- support -->
<smallrye.config.version>3.7.1</smallrye.config.version>
- <guava.version>33.3.0-jre</guava.version>
+ <guava.version>33.4.8-jre</guava.version>
<hamcrest.version>3.0</hamcrest.version>
<xmlunit.version>2.10.0</xmlunit.version>
<httpclient.version>4.5.14</httpclient.version>
@@ -2166,17 +2185,17 @@
<javassist.version>3.30.2-GA</javassist.version>
<jettison.version>1.3.7</jettison.version> <!-- TODO: 1.3.8 doesn't work; AbstractJsonTest complexBeanWithAttributes -->
<jboss.vfs.version>3.3.2.Final</jboss.vfs.version>
- <jboss.logging.version>3.6.0.Final</jboss.logging.version>
+ <jboss.logging.version>3.6.1.Final</jboss.logging.version>
<jmh.version>1.37</jmh.version>
<jmockit.version>1.49</jmockit.version>
<junit4.version>4.13.2</junit4.version>
- <junit5.version>5.11.4</junit5.version>
- <junit-platform-suite.version>1.11.0</junit-platform-suite.version>
+ <junit5.version>5.12.2</junit5.version>
+ <junit-platform-suite.version>1.12.2</junit-platform-suite.version>
<junit-platform-suite.legacy.version>1.10.0</junit-platform-suite.legacy.version>
<kryo.version>4.0.3</kryo.version>
<mockito.version>4.11.0</mockito.version> <!-- CQ 17673 -->
<mustache.version>0.9.14</mustache.version>
- <netty.version>4.1.112.Final</netty.version>
+ <netty.version>4.1.122.Final</netty.version>
<opentracing.version>0.33.0</opentracing.version>
<osgi.version>6.0.0</osgi.version>
<osgi.framework.version>1.10.0</osgi.framework.version>
@@ -2192,9 +2211,9 @@
<servlet6.version>6.0.0</servlet6.version>
<simple.version>6.0.1</simple.version>
- <slf4j.version>2.0.16</slf4j.version>
- <spring6.version>6.0.18</spring6.version>
- <testng.version>7.10.2</testng.version>
+ <slf4j.version>2.0.17</slf4j.version>
+ <spring6.version>6.0.23</spring6.version>
+ <testng.version>7.11.0</testng.version>
<testng6.version>6.14.3</testng6.version>
<thymeleaf.version>3.1.2.RELEASE</thymeleaf.version>
<!-- Jakartified, eligible for CQ -->
@@ -2242,14 +2261,14 @@
<jaxrs.api.spec.version>3.1</jaxrs.api.spec.version>
<jaxrs.api.impl.version>3.1.0</jaxrs.api.impl.version>
<jetty.osgi.version>org.eclipse.jetty.*;version="[11,15)"</jetty.osgi.version>
- <jetty.version>12.0.14</jetty.version>
- <jetty9.version>9.4.56.v20240826</jetty9.version>
- <jetty11.version>11.0.24</jetty11.version>
- <jetty.plugin.version>12.0.8</jetty.plugin.version>
+ <jetty.version>12.0.22</jetty.version>
+ <jetty9.version>9.4.57.v20241219</jetty9.version>
+ <jetty11.version>11.0.25</jetty11.version>
+ <jetty.plugin.version>12.0.22</jetty.plugin.version>
<jsonb.api.version>3.0.1</jsonb.api.version>
<jsonp.ri.version>1.1.7</jsonp.ri.version>
<jsonp.jaxrs.version>1.1.7</jsonp.jaxrs.version>
- <moxy.version>4.0.4</moxy.version>
+ <moxy.version>4.0.6</moxy.version>
<yasson.version>3.0.4</yasson.version>
<!-- END of Jakartified -->
diff --git a/security/oauth2-client/pom.xml b/security/oauth2-client/pom.xml
index 1b752a2..ae8fb37 100644
--- a/security/oauth2-client/pom.xml
+++ b/security/oauth2-client/pom.xml
@@ -47,18 +47,6 @@
<dependencies>
-<!-- <dependency>-->
-<!-- <groupId>org.glassfish.jersey.media</groupId>-->
-<!-- <artifactId>jersey-media-json-jackson</artifactId>-->
-<!-- <version>${project.version}</version>-->
-<!-- </dependency>-->
-
- <dependency>
- <groupId>org.glassfish.jersey.media</groupId>
- <artifactId>jersey-media-json-binding</artifactId>
- <version>${project.version}</version>
- </dependency>
-
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
@@ -70,11 +58,6 @@
</exclusion>
</exclusions>
</dependency>
-
- <!--<dependency>-->
- <!--<groupId>jakarta.ws.rs</groupId>-->
- <!--<artifactId>jakarta.ws.rs-api</artifactId>-->
- <!--</dependency>-->
</dependencies>
diff --git a/test-framework/maven/container-runner-maven-plugin/pom.xml b/test-framework/maven/container-runner-maven-plugin/pom.xml
index 2790aed..50277c6 100644
--- a/test-framework/maven/container-runner-maven-plugin/pom.xml
+++ b/test-framework/maven/container-runner-maven-plugin/pom.xml
@@ -168,10 +168,10 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-archiver</artifactId>
</exclusion>
- <exclusion>
+ <!--<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
- </exclusion>
+ </exclusion>-->
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
diff --git a/test-framework/maven/custom-enforcer-rules/pom.xml b/test-framework/maven/custom-enforcer-rules/pom.xml
index 439443e..9638c14 100644
--- a/test-framework/maven/custom-enforcer-rules/pom.xml
+++ b/test-framework/maven/custom-enforcer-rules/pom.xml
@@ -49,14 +49,14 @@
<artifactId>plexus-utils</artifactId>
</exclusion>
<exclusion>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- </exclusion>
- <exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
<exclusion>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </exclusion>
+ <exclusion>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
</exclusion>
@@ -68,12 +68,6 @@
</dependency>
<dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>${commons.io.version}</version>
- </dependency>
-
- <dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>compile</scope>
diff --git a/test-framework/maven/custom-enforcer-rules/src/main/java/org/glassfish/jersey/test/maven/rule/FilePatternDoesNotExistRule.java b/test-framework/maven/custom-enforcer-rules/src/main/java/org/glassfish/jersey/test/maven/rule/FilePatternDoesNotExistRule.java
index f9c8fde..ee275ba 100644
--- a/test-framework/maven/custom-enforcer-rules/src/main/java/org/glassfish/jersey/test/maven/rule/FilePatternDoesNotExistRule.java
+++ b/test-framework/maven/custom-enforcer-rules/src/main/java/org/glassfish/jersey/test/maven/rule/FilePatternDoesNotExistRule.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -17,17 +17,19 @@
package org.glassfish.jersey.test.maven.rule;
import java.io.File;
-import java.io.FileFilter;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
-import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
/**
- * Maven enforcer rule to enforce that given set of files does not exist.<br/>
+ * Maven enforcer rule to enforce that a given set of files does not exist.<br/>
* This rule is similar to apache {@code requireFilesDontExist} with a support for wildcards.
*
* @author Stepan Vavra
@@ -55,8 +57,13 @@
}
final Set<File> matchedFiles = new TreeSet<>();
- for (File matchedFile : dir.listFiles((FileFilter) new WildcardFileFilter(fileItselfPattern))) {
- matchedFiles.add(matchedFile);
+ try {
+ final DirectoryStream<Path> directoryStream
+ = Files.newDirectoryStream(dir.toPath(), fileItselfPattern);
+ directoryStream.forEach(path -> matchedFiles.add(path.toFile()));
+ directoryStream.close();
+ } catch (IOException e) {
+ throw new EnforcerRuleException(e);
}
if (!matchedFiles.isEmpty()) {
diff --git a/tests/e2e-client/pom.xml b/tests/e2e-client/pom.xml
index 963af19..3dce2ff 100644
--- a/tests/e2e-client/pom.xml
+++ b/tests/e2e-client/pom.xml
@@ -186,6 +186,19 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-binding</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
@@ -208,11 +221,7 @@
<version>${junit-platform-suite.version}</version>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>${commons.io.version}</version>
- </dependency>
+
</dependencies>
<profiles>
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java
new file mode 100644
index 0000000..eddcba5
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectFileUploadServerTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.client;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+
+/**
+ * Server for the file upload test that redirects from /submit to /upload.
+ */
+class RedirectFileUploadServerTest {
+ private static final String UPLOAD_DIRECTORY = "target/uploads";
+ private static final String BOUNDARY_PREFIX = "boundary=";
+ private static final Path uploadDir = Paths.get(UPLOAD_DIRECTORY);
+
+ private static HttpServer server;
+
+
+ static void start(int port) throws IOException {
+ // Create upload directory if it doesn't exist
+ if (!Files.exists(uploadDir)) {
+ Files.createDirectory(uploadDir);
+ }
+
+ // Create HTTP server
+ server = HttpServer.create(new InetSocketAddress(port), 0);
+
+ // Create contexts for different endpoints
+ server.createContext("/submit", new SubmitHandler());
+ server.createContext("/upload", new UploadHandler());
+
+ // Set executor and start server
+ server.setExecutor(Executors.newFixedThreadPool(10));
+ server.start();
+ System.out.println("Server running on port " + port);
+ }
+
+ public static void stop() {
+ server.stop(0);
+ }
+
+
+ // Handler for /submit endpoint that redirects to /upload
+ static class SubmitHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ if (!"POST".equals(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
+ return;
+ }
+
+ final BufferedReader reader
+ = new BufferedReader(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
+ while (reader.readLine() != null) {
+ //discard payload - required for JDK 1.8
+ }
+ reader.close();
+
+ // Send a 307 Temporary Redirect to /upload
+ // This preserves the POST method and body in the redirect
+ exchange.getResponseHeaders().add("Location", "/upload");
+ exchange.sendResponseHeaders(307, -1);
+ } finally {
+ exchange.close();
+ }
+ }
+ }
+
+ // Handler for /upload endpoint that processes file uploads
+ static class UploadHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ try {
+ if (!"POST".equals(exchange.getRequestMethod())) {
+ sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
+ return;
+ }
+
+ // Check if the request contains multipart form data
+ String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
+ if (contentType == null || !contentType.startsWith("multipart/form-data")) {
+ sendResponse(exchange, 400, "Bad Request. Content type must be multipart/form-data.");
+ return;
+ }
+
+ // Extract boundary from content type
+ String boundary = extractBoundary(contentType);
+ if (boundary == null) {
+ sendResponse(exchange, 400, "Bad Request. Could not determine boundary.");
+ return;
+ }
+
+ // Process the multipart request and save the file
+ String fileName = processMultipartRequest(exchange, boundary);
+
+ if (fileName != null) {
+ sendResponse(exchange, 200, "File uploaded successfully: " + fileName);
+ } else {
+ sendResponse(exchange, 400, "Bad Request. No file found in request.");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
+ } finally {
+ exchange.close();
+ Files.deleteIfExists(uploadDir);
+ }
+ }
+
+ private String extractBoundary(String contentType) {
+ int boundaryIndex = contentType.indexOf(BOUNDARY_PREFIX);
+ if (boundaryIndex != -1) {
+ return "--" + contentType.substring(boundaryIndex + BOUNDARY_PREFIX.length());
+ }
+ return null;
+ }
+
+ private String processMultipartRequest(HttpExchange exchange, String boundary) throws IOException {
+ InputStream requestBody = exchange.getRequestBody();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(requestBody, StandardCharsets.UTF_8));
+
+ String line;
+ String fileName = null;
+ Path tempFile = null;
+ boolean isFileContent = false;
+
+ // Generate a random filename for the temporary file
+ String tempFileName = UUID.randomUUID().toString();
+ tempFile = Files.createTempFile(tempFileName, ".tmp");
+
+ try (OutputStream fileOut = Files.newOutputStream(tempFile)) {
+ while ((line = reader.readLine()) != null) {
+ // Check for the boundary
+ if (line.startsWith(boundary)) {
+ if (isFileContent) {
+ // We've reached the end of the file content
+ break;
+ }
+
+ // Read the next line (Content-Disposition)
+ line = reader.readLine();
+ if (line != null && line.startsWith("Content-Type")) {
+ line = reader.readLine();
+ }
+ if (line != null && line.contains("filename=")) {
+ // Extract filename
+ int filenameStart = line.indexOf("filename=\"") + 10;
+ int filenameEnd = line.indexOf("\"", filenameStart);
+ fileName = line.substring(filenameStart, filenameEnd);
+
+ // Skip Content-Type line and empty line
+ reader.readLine(); // Content-Type
+// System.out.println(reader.readLine()); // Empty line
+ isFileContent = true;
+ }
+ } else if (isFileContent) {
+ // If we're reading file content and this line is not a boundary,
+ // write it to the file (append a newline unless it's the first line)
+ fileOut.write(line.getBytes(StandardCharsets.UTF_8));
+ fileOut.write('\n');
+ }
+ }
+ }
+
+ // If we found a file, move it from the temp location to the uploads directory
+ if (fileName != null && !fileName.isEmpty()) {
+ Path targetPath = Paths.get(UPLOAD_DIRECTORY, fileName);
+ Files.move(tempFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
+ return fileName;
+ } else {
+ // If no file was found, delete the temp file
+ Files.deleteIfExists(tempFile);
+ return null;
+ }
+ }
+ }
+
+ // Helper method to send HTTP responses
+ private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
+ exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
+ exchange.sendResponseHeaders(statusCode, response.length());
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java
new file mode 100644
index 0000000..52e6cd2
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RedirectLargeFileTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.client;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class RedirectLargeFileTest {
+
+ private static final int SERVER_PORT = 9997;
+ private static final String SERVER_ADDR = String.format("http://localhost:%d/submit", SERVER_PORT);
+
+ Client client() {
+ final ClientConfig config = new ClientConfig();
+ config.connectorProvider(new NettyConnectorProvider());
+ config.register(MultiPartFeature.class);
+ return ClientBuilder.newClient(config);
+ }
+
+ @BeforeAll
+ static void startServer() throws Exception{
+ RedirectFileUploadServerTest.start(SERVER_PORT);
+ }
+
+ @AfterAll
+ static void stopServer() {
+ RedirectFileUploadServerTest.stop();
+ }
+
+ @Test
+ void sendFileTest() throws Exception {
+
+ final String fileName = "bigFile.json";
+ final String path = "target/" + fileName;
+
+ final Path pathResource = Paths.get(path);
+ try {
+ final Path realFilePath = Files.createFile(pathResource.toAbsolutePath());
+
+ generateJson(realFilePath.toString(), 1000000); // 33Mb real file size
+
+ final byte[] content = Files.readAllBytes(realFilePath);
+
+ final FormDataMultiPart mp = new FormDataMultiPart();
+ mp.bodyPart(new FormDataBodyPart(FormDataContentDisposition.name(fileName).fileName(fileName).build(),
+ content,
+ MediaType.TEXT_PLAIN_TYPE));
+
+ try (final Response response = client().target(SERVER_ADDR).request()
+ .post(Entity.entity(mp, MediaType.MULTIPART_FORM_DATA_TYPE))) {
+ Assertions.assertEquals(200, response.getStatus());
+ }
+ } finally {
+ Files.deleteIfExists(pathResource);
+ }
+ }
+
+ private static void generateJson(final String filePath, int recordCount) throws Exception {
+
+ try (final JsonGenerator generator = new JsonFactory().createGenerator(new FileWriter(filePath))) {
+ generator.writeStartArray();
+
+ for (int i = 0; i < recordCount; i++) {
+ generator.writeStartObject();
+ generator.writeNumberField("id", i);
+ generator.writeStringField("name", "User" + i);
+ // Add more fields as needed
+ generator.writeEndObject();
+
+ if (i % 10000 == 0) {
+ generator.flush();
+ }
+ }
+
+ generator.writeEndArray();
+ }
+ }
+}
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/AbstractConnectorServerTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/AbstractConnectorServerTest.java
index 0fde39c..05b4267 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/AbstractConnectorServerTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/AbstractConnectorServerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -31,11 +31,10 @@
import org.glassfish.jersey.jetty.connector.JettyConnectorProvider;
import org.glassfish.jersey.jnh.connector.JavaNetHttpConnectorProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
-import org.apache.commons.io.IOUtils;
-
/**
* SSL connector hostname verification tests.
*
@@ -95,9 +94,9 @@
final InputStream trustStore = SslConnectorConfigurationTest.class.getResourceAsStream(clientTrustStore());
final InputStream keyStore = SslConnectorConfigurationTest.class.getResourceAsStream(CLIENT_KEY_STORE);
return SslConfigurator.newInstance()
- .trustStoreBytes(IOUtils.toByteArray(trustStore))
+ .trustStoreBytes(ReaderWriter.readFromAsBytes(trustStore))
.trustStorePassword("asdfgh")
- .keyStoreBytes(IOUtils.toByteArray(keyStore))
+ .keyStoreBytes(ReaderWriter.readFromAsBytes(keyStore))
.keyPassword("asdfgh")
.createSSLContext();
}
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/Server.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/Server.java
index 20795c3..b66ff73 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/Server.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/ssl/Server.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,9 +23,9 @@
import jakarta.ws.rs.core.UriBuilder;
-import org.apache.commons.io.IOUtils;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.message.internal.ReaderWriter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.grizzly.http.server.HttpServer;
@@ -86,9 +86,9 @@
SSLContextConfigurator sslContext = new SSLContextConfigurator();
// set up security context
- sslContext.setKeyStoreBytes(IOUtils.toByteArray(keyStore)); // contains server key pair
+ sslContext.setKeyStoreBytes(ReaderWriter.readFromAsBytes(keyStore)); // contains server key pair
sslContext.setKeyStorePass("asdfgh");
- sslContext.setTrustStoreBytes(IOUtils.toByteArray(trustStore)); // contains client certificate
+ sslContext.setTrustStoreBytes(ReaderWriter.readFromAsBytes(trustStore)); // contains client certificate
sslContext.setTrustStorePass("asdfgh");
ResourceConfig rc = new ResourceConfig();
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
index ffb8b1f..426b494 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,33 +16,48 @@
package org.glassfish.jersey.tests.e2e.client.nettyconnector;
-import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.util.Callback;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.http.Expect100ContinueFeature;
import org.glassfish.jersey.netty.connector.NettyClientProperties;
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
-import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import javax.net.ServerSocketFactory;
import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.AsyncInvoker;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.InvocationCallback;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
-import java.nio.ByteBuffer;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
public class Expect100ContinueTest /*extends JerseyTest*/ {
@@ -57,187 +72,385 @@
private static final String RESOURCE_PATH_METHOD_NOT_SUPPORTED = "fail405";
private static final String ENTITY_STRING = "1234567890123456789012345678901234567890123456789012"
- + "3456789012345678901234567890";
+ + "3456789012345678901234567890";
private static final Integer portNumber = 9997;
- private static Server server;
- @BeforeAll
- private static void startExpect100ContinueTestServer() {
- server = new Server(portNumber);
- server.setDefaultHandler(new Expect100ContinueTestHandler());
- server.setDynamic(true);
- try {
- server.start();
- } catch (Exception e) {
+ private static TestSocketServer server;
- }
- }
-
- @AfterAll
- private static void stopExpect100ContinueTestServer() {
- try {
- server.stop();
- } catch (Exception e) {
- }
- }
+ private static Client client;
@BeforeAll
- private static void initClient() {
+ static void beforeAll() {
final ClientConfig config = new ClientConfig();
config.connectorProvider(new NettyConnectorProvider());
client = ClientBuilder.newClient(config);
}
- @AfterAll
- private static void stopClient() {
- client.close();
+ @BeforeEach
+ void beforeEach() throws IOException {
+ server = new TestSocketServer(portNumber);
+ server.runServer();
}
- private static Client client;
+ @AfterEach
+ void afterEach() {
+ server.stop();
+ }
+
public WebTarget target(String path) {
return client.target(String.format("http://localhost:%d", portNumber)).path(path);
}
@Test
public void testExpect100Continue() {
- final Response response = target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING));
- assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK
+ final Response response = target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING));
+ assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK
}
@Test
public void testExpect100ContinueChunked() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
- .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
- RequestEntityProcessing.CHUNKED).request().post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.CHUNKED)
+ .request().post(Entity.text(ENTITY_STRING));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ }
+
+ @Test
+ public void testExpect100ContinueManyAsyncRequests() {
+
+ final Invocation.Builder requestBuilder = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.CHUNKED)
+ .request();
+ final AsyncInvoker invoker =
+ requestBuilder.async();
+
+ final InvocationCallback<Response> responseCallback = new InvocationCallback<Response>() {
+ @Override
+ public void completed(Response response) {
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ }
+
+ @Override
+ public void failed(Throwable throwable) {
+ fail(throwable); // should not fail
+ }
+ };
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+ invoker.post(Entity.text(ENTITY_STRING), responseCallback);
+
+ final Response response = requestBuilder.post(Entity.text(ENTITY_STRING));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueBuffered() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
- .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
- RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+ .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+ RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L)
+ .post(Entity.text(generateStringByContentLength(67000)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueCustomLength() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
- .request().header(HttpHeaders.CONTENT_LENGTH, Integer.MAX_VALUE)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 200)
+ .post(Entity.text(generateStringByContentLength(200)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueCustomLengthWrong() {
- final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
- .request().header(HttpHeaders.CONTENT_LENGTH, 99L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size
+ final Response response = target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 99L)
+ .post(Entity.text(generateStringByContentLength(99)));
+ assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size
}
@Test
public void testExpect100ContinueCustomLengthProperty() {
- final Response response = target(RESOURCE_PATH)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .register(Expect100ContinueFeature.withCustomThreshold(555L))
- .request().header(HttpHeaders.CONTENT_LENGTH, 666L)
- .post(Entity.text(ENTITY_STRING));
- assertNotNull(response.getStatus()); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .register(Expect100ContinueFeature.withCustomThreshold(555L))
+ .request().header(HttpHeaders.CONTENT_LENGTH, 666L)
+ .post(Entity.text(generateStringByContentLength(666)));
+ assertNotNull(response.getStatus()); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueRegisterViaCustomProperty() {
- final Response response = target(RESOURCE_PATH)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+ final Response response = target(RESOURCE_PATH)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44)));
+ assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
}
@Test
public void testExpect100ContinueNotSupported() {
- final Response response = target(RESOURCE_PATH_NOT_SUPPORTED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING));
- assertEquals(417, response.getStatus(), "Expected 417"); //Expectations not supported
+ final Response response = target(RESOURCE_PATH_NOT_SUPPORTED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44)));
+ assertEquals(204, response.getStatus(),
+ "This should re-send request without expect and obtain the 204 response code"); //Expectations not supported
}
@Test
public void testExpect100ContinueUnauthorized() {
- assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
}
@Test
public void testExpect100ContinuePayloadTooLarge() {
assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_PAYLOAD_TOO_LARGE)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
}
@Test
public void testExpect100ContinueMethodNotSupported() {
- assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED)
- .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
- .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
- .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
- .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
- .post(Entity.text(ENTITY_STRING)));
+ assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED)
+ .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+ .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+ .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+ .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+ .post(Entity.text(generateStringByContentLength(44))));
+
}
- static class Expect100ContinueTestHandler extends Handler.Abstract {
- @Override
- public boolean handle(Request request,
- org.eclipse.jetty.server.Response response,
- Callback callback) throws IOException {
- boolean expected = request.getHeaders().contains("Expect");
- boolean failed = false;
- final String target = request.getHttpURI().getCanonicalPath();
- if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) {
- response.setStatus(417);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) {
- response.setStatus(401);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) {
- response.setStatus(413);
- failed = true;
- }
- if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) {
- response.setStatus(405);
- failed = true;
- }
- if (expected && !failed) {
- System.out.println("Expect:100-continue found, sending response header");
- response.setStatus(204);
- callback.succeeded();
- return true;
- }
- if (!expected && !failed) {
- response.reset();
- callback.succeeded();
- return true;
- }
- response.write(true, ByteBuffer.wrap("\n\r".getBytes()), callback);
-
- callback.failed(new ProcessingException(""));
- return true;
+ private String generateStringByContentLength(int length) {
+ final char[] array = new char[length];
+ final Random r = new Random();
+ for (int i = 0; i < length; i++) {
+ array[i] = ENTITY_STRING.charAt(r.nextInt(ENTITY_STRING.length()));
}
+ return String.valueOf(array);
+ }
+
+ private static final class TestSocketServer {
+
+ private static final String NO_CONTENT_HEADER = "HTTP/1.1 204 No Content";
+ private static final String OK_HEADER = "HTTP/1.1 200 OK";
+ private static final String EXPECT_HEADER = "HTTP/1.1 100 Continue";
+ private static final String UNAUTHORIZED_HEADER = "HTTP/1.1 401 Unauthorized";
+ private static final String NOT_SUPPORTED_HEADER = "HTTP/1.1 405 Method Not Allowed";
+ private static final String TOO_LARGE_HEADER = "HTTP/1.1 413 Request Entity Too Large";
+
+ private final ExecutorService executorService = Executors.newCachedThreadPool();
+ private AtomicBoolean unauthorized = new AtomicBoolean(false);
+ private AtomicBoolean not_supported = new AtomicBoolean(false);
+ private AtomicBoolean too_large = new AtomicBoolean(false);
+
+ private AtomicBoolean expect_processed = new AtomicBoolean(false);
+
+ private ServerSocket server;
+
+ private volatile boolean stopped = false;
+
+ public TestSocketServer(int port) throws IOException {
+ final ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
+ server = socketFactory.createServerSocket(port);
+ }
+
+ void stop() {
+ stopped = true;
+ try {
+ server.close();
+ executorService.shutdown();
+ while (!executorService.isTerminated()) {
+ executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
+ }
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void runServer() {
+
+ executorService.execute(() -> {
+ try {
+ while (!stopped) {
+ final Socket socket = server.accept();
+ executorService.submit(() -> processRequest(socket));
+ }
+ } catch (IOException e) {
+ if (!stopped) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private void processRequest(final Socket request) {
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(request.getOutputStream()))) {
+
+
+ while (!stopped) {
+ final Map<String, String> headers = mapHeaders(reader);
+
+ if (headers.isEmpty()) {
+ continue;
+ }
+
+ boolean failed = processExpect100Continue(headers, writer);
+
+ if (failed) {
+ continue;
+ }
+
+ final String http_header = expect_processed.get() ? NO_CONTENT_HEADER : OK_HEADER;
+ boolean read = readBody(reader, headers);
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+ addNewLineToResponse(responseBuffer);
+ addServerHeaderToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+
+ writer.write(responseBuffer.toString());
+
+ writer.flush();
+ if (read) {
+ break;
+ }
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ request.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addNewLineToResponse(StringBuffer responseBuffer) {
+ addToResponse("\r\n", responseBuffer);
+ }
+
+ private void addToResponse(String toBeAdded, StringBuffer responseBuffer) {
+ responseBuffer.append(toBeAdded);
+ }
+
+ private void addServerHeaderToResponse(StringBuffer responseBuffer) {
+ addToResponse("Server: SocketServer v.0.0.1", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ }
+
+ private boolean processExpect100Continue(Map<String, String> headers, BufferedWriter writer) throws IOException {
+ String http_header = EXPECT_HEADER;
+ boolean failed = false;
+ final String continueHeader = headers.remove("expect");
+
+ if (continueHeader != null && continueHeader.contains("100-continue")) {
+
+ if (unauthorized.get()) {
+ http_header = UNAUTHORIZED_HEADER;
+ unauthorized.set(false);
+ failed = true;
+ }
+
+ if (not_supported.get()) {
+ http_header = NOT_SUPPORTED_HEADER;
+ not_supported.set(false);
+ failed = true;
+ }
+
+ if (too_large.get()) {
+ http_header = TOO_LARGE_HEADER;
+ too_large.set(false);
+ failed = true;
+ }
+
+ expect_processed.set(http_header.equals(EXPECT_HEADER));
+
+
+ final StringBuffer responseBuffer = new StringBuffer(http_header);
+
+ addNewLineToResponse(responseBuffer);
+ addToResponse("Connection: keep-alive", responseBuffer);
+ addNewLineToResponse(responseBuffer);
+ addNewLineToResponse(responseBuffer);
+
+ writer.write(responseBuffer.toString());
+ writer.flush();
+ }
+ return failed;
+ }
+
+ private Map<String, String> mapHeaders(BufferedReader reader) throws IOException {
+ String line;
+ final Map<String, String> headers = new HashMap<>();
+
+
+ if (!reader.ready()) {
+ return headers;
+ }
+
+ while ((line = reader.readLine()) != null && !line.isEmpty()) {
+
+ if (line.contains(RESOURCE_PATH_UNAUTHORIZED)) {
+ unauthorized.set(true);
+ }
+
+ if (line.contains(RESOURCE_PATH_METHOD_NOT_SUPPORTED)) {
+ not_supported.set(true);
+ }
+
+ if (line.contains(RESOURCE_PATH_PAYLOAD_TOO_LARGE)) {
+ too_large.set(true);
+ }
+
+ int pos = line.indexOf(':');
+ if (pos > -1) {
+ headers.put(
+ line.substring(0, pos).toLowerCase(Locale.ROOT),
+ line.substring(pos + 2).toLowerCase(Locale.ROOT).trim());
+ }
+ }
+
+ return headers;
+ }
+
+ private boolean readBody(BufferedReader reader, Map<String, String> headers) throws IOException, InterruptedException {
+ if (headers.containsKey("content-length")) {
+ int contentLength = Integer.valueOf(headers.get("content-length"));
+ int actualLength = 0, readingByte = 0;
+ int[] buffer = new int[contentLength];
+ while (actualLength < contentLength && (readingByte = reader.read()) != -1) {
+ buffer[actualLength++] = readingByte;
+ }
+ return (actualLength == contentLength);
+ } else if (headers.containsKey("transfer-encoding")) {
+ String line;
+ while ((line = reader.readLine()) != null && !line.equals("0")) {
+ }
+ return true;
+ }
+ return false;
+ }
+
}
}
\ No newline at end of file
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
index 7582894..d0488fa 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/process/internal/RequestScopeTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -17,6 +17,8 @@
package org.glassfish.jersey.tests.e2e.common.process.internal;
import java.lang.reflect.Type;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import org.glassfish.jersey.inject.hk2.Hk2RequestScope;
import org.glassfish.jersey.internal.inject.ForeignDescriptor;
@@ -25,6 +27,7 @@
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.utilities.AbstractActiveDescriptor;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -140,6 +143,34 @@
assertNull(instance.get(inhab));
}
+ @Test
+ public void testOrderOfRelease() {
+ final RequestScope requestScope = new Hk2RequestScope();
+ final AtomicInteger instanceRelease = new AtomicInteger(0);
+ final Hk2RequestScope.Instance instance = requestScope.runInScope(() -> {
+ final Hk2RequestScope.Instance internalInstance = (Hk2RequestScope.Instance) requestScope.current();
+ for (int index = 1; index != 10; index++) {
+ final int in = index;
+ TestProvider testProvider = new TestProvider(String.valueOf(in)) {
+ @Override
+ public int hashCode() {
+ return super.hashCode() + in;
+ }
+ };
+ final ForeignDescriptor fd = ForeignDescriptor.wrap(testProvider, new Consumer<Object>() {
+ @Override
+ public void accept(Object o) {
+ instanceRelease.set(instanceRelease.get() * 10 + in);
+ }
+ });
+ internalInstance.put(fd, String.valueOf(index));
+ }
+ return internalInstance;
+ });
+ instance.release();
+ Assertions.assertEquals(987654321, instanceRelease.get());
+ }
+
/**
* Test request scope inhabitant.
*/
diff --git a/tests/e2e-server/pom.xml b/tests/e2e-server/pom.xml
index 4f5617b..3f6d2b0 100644
--- a/tests/e2e-server/pom.xml
+++ b/tests/e2e-server/pom.xml
@@ -210,6 +210,13 @@
</dependency>
<dependency>
+ <groupId>org.eclipse.jetty.ee10</groupId>
+ <artifactId>jetty-ee10-servlet</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
@@ -224,6 +231,34 @@
<profiles>
<profile>
+ <id>JettyTestExclude</id>
+ <activation>
+ <jdk>[11,17)</jdk>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-testCompile</id>
+ <phase>test-compile</phase>
+ <configuration>
+ <testExcludes>
+ <testExclude>org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java</testExclude>
+ </testExcludes>
+ </configuration>
+ <goals>
+ <goal>testCompile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
<id>xdk</id>
<properties>
<!-- do not use security manager for xdk -->
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java
new file mode 100644
index 0000000..8e8b5d4
--- /dev/null
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SimilarInputStreamTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.server;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.internal.InternalProperties;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
+import org.glassfish.jersey.message.internal.ReaderWriter;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.spi.TestContainer;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+import org.junit.jupiter.api.Test;
+
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SimilarInputStreamTest extends JerseyTest {
+
+ @Override
+ protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+ return (baseUri, deploymentContext) -> {
+ final Server server = JettyHttpContainerFactory.createServer(baseUri, false);
+ final ServerConnector connector = new ServerConnector(server);
+ connector.setPort(9001);
+ server.addConnector(connector);
+
+ final ServletContainer jerseyServletContainer = new ServletContainer(deploymentContext.getResourceConfig());
+ final ServletHolder jettyServletHolder = new ServletHolder(jerseyServletContainer);
+
+ final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/");
+
+ // filter which will change the http servlet request to have a reply-able input stream
+ context.addFilter(FilterSettingMultiReadRequest.class,
+ "/*", EnumSet.allOf(DispatcherType.class));
+ context.addServlet(jettyServletHolder, "/api/*");
+
+ server.setHandler(context);
+ return new TestContainer() {
+ @Override
+ public ClientConfig getClientConfig() {
+ return new ClientConfig();
+ }
+
+ @Override
+ public URI getBaseUri() {
+ return baseUri;
+ }
+
+ @Override
+ public void start() {
+ try {
+ server.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ };
+ }
+
+ @Override
+ protected Application configure() {
+ ResourceConfig resourceConfig = new ResourceConfig(TestResource.class);
+ // force jersey to use jackson for deserialization
+ resourceConfig.addProperties(
+ Collections.singletonMap(InternalProperties.JSON_FEATURE, JacksonFeature.class.getSimpleName()));
+ return resourceConfig;
+ }
+
+ @Test
+ public void readJsonWithReplayableInputStreamFailsTest() {
+ final Invocation.Builder requestBuilder = target("/api/v1/echo").request();
+ final MyDto myDto = new MyDto();
+ myDto.setMyField("Something");
+ try (Response response = requestBuilder.post(Entity.entity(myDto, MediaType.APPLICATION_JSON))) {
+ // fixed from failure with a 400 as jackson can never finish reading the input stream
+ assertEquals(200, response.getStatus());
+ final MyDto resultDto = response.readEntity(MyDto.class);
+ assertEquals("Something", resultDto.getMyField()); //verify we still get Something
+ }
+ }
+
+ @Path("/v1")
+ public static class TestResource {
+
+ @POST
+ @Path("/echo")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public MyDto echo(MyDto input) {
+ return input;
+ }
+ }
+
+ public static class MyDto {
+ private String myField;
+
+ public String getMyField() {
+ return myField;
+ }
+
+ public void setMyField(String myField) {
+ this.myField = myField;
+ }
+
+ @Override
+ public String toString() {
+ return "MyDto{"
+ + "myField='" + myField + '\''
+ + '}';
+ }
+ }
+
+
+ public static class FilterSettingMultiReadRequest implements Filter {
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ /* wrap the request in order to read the inputstream multiple times */
+ MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
+ chain.doFilter(multiReadRequest, response);
+ }
+ }
+
+ static class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
+ private byte[] cachedBytes;
+
+ public MultiReadHttpServletRequest(HttpServletRequest request) {
+ super(request);
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ if (cachedBytes == null) {
+ cacheInputStream();
+ }
+
+ return new CachedServletInputStream(cachedBytes);
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException {
+ return new BufferedReader(new InputStreamReader(getInputStream()));
+ }
+
+ private void cacheInputStream() throws IOException {
+ // Cache the inputstream in order to read it multiple times.
+ cachedBytes = ReaderWriter.readFromAsBytes(super.getInputStream());
+ }
+
+
+ /* An input stream which reads the cached request body */
+ private class CachedServletInputStream extends ServletInputStream {
+
+ private final ByteArrayInputStream buffer;
+
+ public CachedServletInputStream(byte[] contents) {
+ this.buffer = new ByteArrayInputStream(contents);
+ }
+
+ @Override
+ public int read() {
+ return buffer.read();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return buffer.available() == 0;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void setReadListener(ReadListener listener) {
+ throw new RuntimeException("Not implemented");
+ }
+ }
+ }
+}
diff --git a/tests/e2e/pom.xml b/tests/e2e/pom.xml
index 60cf525..1a6e9ff 100644
--- a/tests/e2e/pom.xml
+++ b/tests/e2e/pom.xml
@@ -175,6 +175,12 @@
<version>${junit-platform-suite.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-binding</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/CookieImplTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/CookieImplTest.java
index 5992b54..53cd76d 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/CookieImplTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/CookieImplTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2014, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -151,27 +151,46 @@
@Test
public void testMultipleCookiesWithSameName(){
- String cookieHeader = "kobe=longeststring; kobe=shortstring";
+ String cookieHeader = "kobe=oldeststring; kobe=neweststring";
Map<String, Cookie> cookies = HttpHeaderReader.readCookies(cookieHeader);
assertEquals(cookies.size(), 1);
Cookie c = cookies.get("kobe");
assertEquals(c.getVersion(), 0);
assertEquals("kobe", c.getName());
- assertEquals("longeststring", c.getValue());
+ assertEquals("neweststring", c.getValue());
- cookieHeader = "bryant=longeststring; bryant=shortstring; fred=shortstring ;fred=longeststring;$Path=/path";
+ cookieHeader = "bryant=longeststring; bryant=neweststring; fred=oldeststring ;fred=neweststring;$Path=/path";
cookies = HttpHeaderReader.readCookies(cookieHeader);
assertEquals(cookies.size(), 2);
c = cookies.get("bryant");
assertEquals(c.getVersion(), 0);
assertEquals("bryant", c.getName());
- assertEquals("longeststring", c.getValue());
+ assertEquals("neweststring", c.getValue());
c = cookies.get("fred");
assertEquals(c.getVersion(), 0);
assertEquals("fred", c.getName());
- assertEquals("longeststring", c.getValue());
+ assertEquals("neweststring", c.getValue());
assertEquals("/path", c.getPath());
+ cookieHeader = "cookiewithpath=longeststring;$Path=/path; cookiewithpath=string1;$Path=/path;"
+ + " cookiewithpath=string2;$Path=/path ;cookiewithpath=string3;$Path=/path";
+ cookies = HttpHeaderReader.readCookies(cookieHeader);
+ assertEquals(cookies.size(), 1);
+ c = cookies.get("cookiewithpath");
+ assertEquals(c.getVersion(), 0);
+ assertEquals("cookiewithpath", c.getName());
+ assertEquals("string3", c.getValue());
+
+ cookieHeader = "cookiewithpath=longeststring;$Path=/path/added/path; cookiewithpath=string1;$Path=/path;"
+ + " cookiewithpath=string2;$Path=/path ;cookiewithpath=string3;$Path=/path";
+ cookies = HttpHeaderReader.readCookies(cookieHeader);
+ assertEquals(cookies.size(), 1);
+ c = cookies.get("cookiewithpath");
+ assertEquals(c.getVersion(), 0);
+ assertEquals("cookiewithpath", c.getName());
+ assertEquals("longeststring", c.getValue());
+ assertEquals("/path/added/path", c.getPath());
+
}
@Test
diff --git a/tests/integration/jersey-5796/pom.xml b/tests/integration/jersey-5796/pom.xml
index 2710d8e..45a371c 100644
--- a/tests/integration/jersey-5796/pom.xml
+++ b/tests/integration/jersey-5796/pom.xml
@@ -23,7 +23,7 @@
<parent>
<artifactId>project</artifactId>
<groupId>org.glassfish.jersey.tests.integration</groupId>
- <version>2.47-SNAPSHOT</version>
+ <version>3.1.99-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
index 00705b1..80ade6a 100644
--- a/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
+++ b/tests/integration/jersey-5796/src/test/java/org/glassfish/jersey/tests/integration/jersey5796/Jersey5796Test.java
@@ -18,26 +18,19 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.lang.reflect.Field;
-import java.util.Map;
-import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.core.Application;
-import javax.ws.rs.core.GenericType;
-import javax.ws.rs.core.Response;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.Response;
import org.glassfish.jersey.client.ChunkedInput;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientLifecycleListener;
-import org.glassfish.jersey.client.JerseyClient;
import org.glassfish.jersey.server.ChunkedOutput;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index d2ad150..b69188a 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -59,6 +59,7 @@
<module>jersey-4697</module>
<module>jersey-4722</module>
<module>jersey-5087</module>
+ <module>jersey-5796</module>
<module>microprofile</module>
<module>property-check</module>
<module>reactive-streams</module>
diff --git a/tests/integration/resteasy-client/pom.xml b/tests/integration/resteasy-client/pom.xml
index 504b4eb..97a2de8 100644
--- a/tests/integration/resteasy-client/pom.xml
+++ b/tests/integration/resteasy-client/pom.xml
@@ -28,10 +28,10 @@
<version>3.1.99-SNAPSHOT</version>
</parent>
- <artifactId>spring-boot</artifactId>
+ <artifactId>resteasy-client</artifactId>
<packaging>war</packaging>
- <name>jersey-tests-integration-spring6</name>
+ <name>jersey-tests-integration-resteasy-client</name>
<description>
Jersey tests for Spring.Boot / Resteasy integration
@@ -41,7 +41,7 @@
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
- <version>6.2.11.Final</version>
+ <version>6.2.12.Final</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
@@ -72,6 +72,11 @@
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-client</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>commons-logging</groupId>
diff --git a/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java b/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
index de1461f..4848388 100644
--- a/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
+++ b/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
@@ -27,9 +27,12 @@
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Providers;
+
import org.glassfish.jersey.jackson.internal.DefaultJacksonJaxbJsonProvider;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl;
import org.junit.jupiter.api.Assertions;
@@ -40,6 +43,11 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
public class RestEasyClientTest extends JerseyTest {
@@ -62,17 +70,31 @@
@Test
public void test() throws InterruptedException {
- try (final ResteasyClient client = new ResteasyClientBuilderImpl().build()) {
- client.register(TestDefaultJacksonJaxbJsonProvider.class);
-
- try (final Response r = client.target(target().getUri()).path("/test")
- .request().post(Entity.entity("{\"test\": \"test\"}", MediaType.APPLICATION_JSON))) {
- Object o = r.readEntity(Object.class);
- Assertions.assertTrue(o.toString().contains("test"));
- readFromLatch.await();
- Assertions.assertEquals(0, readFromLatch.getCount(), "DefaultJacksonJaxbJsonProvider has not been used");
+ AtomicReference<String> messageRef = new AtomicReference<>();
+ Logger logger = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName());
+ logger.addHandler(new ConsoleHandler() {
+ @Override
+ public void publish(LogRecord record) {
+ messageRef.set(record.getMessage());
}
+ });
+ logger.setLevel(Level.FINE);
+
+ final ResteasyClient client = new ResteasyClientBuilderImpl().build();
+
+ client.register(TestDefaultJacksonJaxbJsonProvider.class);
+
+ try (final Response r = client.target(target().getUri()).path("/test")
+ .request().post(Entity.entity("{\"test\": \"test\"}", MediaType.APPLICATION_JSON))) {
+ Object o = r.readEntity(Object.class);
+ Assertions.assertTrue(o.toString().contains("test"));
+ readFromLatch.await();
+ Assertions.assertEquals(0, readFromLatch.getCount(), "DefaultJacksonJaxbJsonProvider has not been used");
}
+
+ client.close();
+ MatcherAssert.assertThat(messageRef.get(), Matchers.notNullValue());
+
}
public static class TestDefaultJacksonJaxbJsonProvider extends DefaultJacksonJaxbJsonProvider {
diff --git a/tests/integration/security-digest/pom.xml b/tests/integration/security-digest/pom.xml
index 276d18a..c378737 100644
--- a/tests/integration/security-digest/pom.xml
+++ b/tests/integration/security-digest/pom.xml
@@ -69,7 +69,7 @@
<loginServices>
<loginService implementation="org.eclipse.jetty.security.HashLoginService">
<name>my-realm</name>
- <config implementation="org.eclipse.jetty.ee10.maven.plugin.MavenResource">
+ <config implementation="org.eclipse.jetty.maven.MavenResource">
<resourceAsString>${basedir}/src/main/resources/jetty/realm.properties</resourceAsString>
</config>
</loginService>
diff --git a/tools/jersey-release-notes-maven-plugin/pom.xml b/tools/jersey-release-notes-maven-plugin/pom.xml
index 1522089..dfd342b 100644
--- a/tools/jersey-release-notes-maven-plugin/pom.xml
+++ b/tools/jersey-release-notes-maven-plugin/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2019, 2025 Oracle and/or its affiliates. All rights reserved.
This program and the accompanying materials are made available under the
terms of the Eclipse Public License v. 2.0, which is available at
@@ -56,7 +56,7 @@
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
- <version>3.6.0</version>
+ <version>3.15.1</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -74,7 +74,8 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
- <version>2.17.0</version>
+ <version>${commons.io.version}</version>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
@@ -83,9 +84,15 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.13.2</version>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>${junit.version}</version>
<scope>test</scope>
</dependency>
@@ -96,7 +103,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
- <version>3.6.0</version>
+ <version>3.15.1</version>
<configuration>
<skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
</configuration>
@@ -113,7 +120,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
- <version>3.1</version>
+ <version>3.14.0</version>
<inherited>true</inherited>
<configuration>
<source>${java.version}</source>
@@ -127,6 +134,8 @@
<properties>
<java.version>1.8</java.version>
- <maven.version>3.8.1</maven.version>
+ <maven.version>3.9.9</maven.version>
+ <commons.io.version>2.19.0</commons.io.version>
+ <junit.version>5.12.2</junit.version>
</properties>
</project>
diff --git a/tools/jersey-release-notes-maven-plugin/src/test/java/org/glassfish/jersey/tools/plugins/releasenotes/GenerateReleaseNotesMojoTest.java b/tools/jersey-release-notes-maven-plugin/src/test/java/org/glassfish/jersey/tools/plugins/releasenotes/GenerateReleaseNotesMojoTest.java
index 93287ee..1826730 100644
--- a/tools/jersey-release-notes-maven-plugin/src/test/java/org/glassfish/jersey/tools/plugins/releasenotes/GenerateReleaseNotesMojoTest.java
+++ b/tools/jersey-release-notes-maven-plugin/src/test/java/org/glassfish/jersey/tools/plugins/releasenotes/GenerateReleaseNotesMojoTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,17 +23,14 @@
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingRequest;
import org.eclipse.aether.DefaultRepositorySystemSession;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import java.io.File;
-@RunWith(JUnit4.class)
public class GenerateReleaseNotesMojoTest extends AbstractMojoTestCase {
- @Before
+ @BeforeEach
public void setUp() throws Exception {
// required for mojo lookups to work