Merge remote-tracking branch 'origin/2.x' into 'origin/3.0' Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/NOTICE.md b/NOTICE.md index e9981e6..b245c38 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 8bceac6..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 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,111 +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().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/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 6e795f1..7ba0f51 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 5188a1a..c3c6c1f 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 @@ -116,6 +116,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..f038bf4 --- /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 javax.servlet.AsyncContext; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import javax.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 fefc684..f4c8017 100644 --- a/core-client/pom.xml +++ b/core-client/pom.xml
@@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved. + Copyright (c) 2011, 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 @@ -153,6 +153,19 @@ <profiles> <profile> + <id>mockito_jdk_11</id> + <activation> + <jdk>11</jdk> + </activation> + <dependencies> + <dependency> + <groupId>jakarta.xml.bind</groupId> + <artifactId>jakarta.xml.bind-api</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + </profile> + <profile> <id>sonar</id> <build> <plugins>
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..35fcef5 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; @@ -77,18 +78,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());
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 49fc03c..212e83d 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 cb1d937..10cdfac 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 @@ -61,7 +61,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..caffa4d --- /dev/null +++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -0,0 +1,102 @@ +/* + * 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 org.junit.jupiter.api.Test; + +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.core.GenericEntity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.MessageBodyWriter; +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.List; + +//import static java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AbortTest { + private static final String TEXT_CSV = "text/csv"; + 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") + ); + + @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) { + System.out.println(genericType.getTypeName()); + 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(); + } + } + +}
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/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-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 b539cdc..70c3d2b 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..7b1714c --- /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>2.47-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..a91fd92 --- /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 javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.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 609875f..3710074 100644 --- a/examples/pom.xml +++ b/examples/pom.xml
@@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved. + Copyright (c) 2011, 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 @@ -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/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/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java index 0b9222c..4defc70 100644 --- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java +++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.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 @@ -50,6 +50,7 @@ public class DefaultJacksonJaxbJsonProvider extends JacksonJaxbJsonProvider { private Configuration commonConfig; private static final Logger LOGGER = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName()); + private final boolean hasConfig; @Inject public DefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) { @@ -64,10 +65,18 @@ this.commonConfig = config; _providers = providers; - Object jaxrsFeatureBag = config.getProperty(JaxrsFeatureBag.JAXRS_FEATURE); - if (jaxrsFeatureBag != null && (JaxrsFeatureBag.class.isInstance(jaxrsFeatureBag))) { - ((JaxrsFeatureBag) jaxrsFeatureBag).configureJaxrsFeatures(this); + boolean ex = true; + try { + Object jaxrsFeatureBag = config.getProperty(JaxrsFeatureBag.JAXRS_FEATURE); + if (jaxrsFeatureBag != null && (JaxrsFeatureBag.class.isInstance(jaxrsFeatureBag))) { + ((JaxrsFeatureBag) jaxrsFeatureBag).configureJaxrsFeatures(this); + } + } catch (RuntimeException e) { + // ignore - not configured + LOGGER.fine(LocalizationMessages.ERROR_CONFIGURING(e.getMessage())); + ex = false; } + hasConfig = ex; } @Override @@ -82,7 +91,9 @@ @Override protected JsonEndpointConfig _configForReading(ObjectReader reader, Annotation[] annotations) { try { - updateFactoryConstraints(reader.getFactory()); + if (hasConfig) { + updateFactoryConstraints(reader.getFactory()); + } } catch (Throwable t) { // A Jackson 14 would throw NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError or similar // that should have been ignored
diff --git a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties index 1943306..1c66b9c 100644 --- a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties +++ b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
@@ -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 @@ -13,6 +13,7 @@ # # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 # +error.configuring=Error configuring the DefaultJacksonJaxbJsonProvider: {0}. error.jackson.streamreadconstraints=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.15? error.jackson.streamreadconstraints218=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.18? error.modules.not.loaded=Jackson modules could not be loaded: {0} \ No newline at end of file
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 2ac83ef..21c1712 100644 --- a/pom.xml +++ b/pom.xml
@@ -1395,6 +1395,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> @@ -2132,16 +2152,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.23.1</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 @@ -2150,24 +2170,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> @@ -2180,25 +2200,25 @@ <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.io.version>2.19.0</commons.io.version> <commons.codec.version>1.16.1</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> + <gae.version>2.0.36</gae.version> + <groovy.version>5.0.0-alpha-12</groovy.version> <groovy.jdk8.version>4.0.24</groovy.jdk8.version> - <gson.version>2.11.0</gson.version> + <gson.version>2.13.1</gson.version> <!--versions, extracted here due to maven-enforcer-plugin --> <!-- <commons.codec.version>1.15</commons.codec.version>--> @@ -2207,7 +2227,7 @@ <!-- 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 --> @@ -2218,7 +2238,7 @@ <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> <hk2.osgi.version>org.glassfish.hk2.*;version="[2.5,4)"</hk2.osgi.version> @@ -2233,17 +2253,17 @@ <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.vfs.jdk8.version>3.2.17.Final</jboss.vfs.jdk8.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> + <junit5.version>5.12.2</junit5.version> <junit5.jdk8.version>5.10.3</junit5.jdk8.version> - <junit-platform-suite.version>1.11.0</junit-platform-suite.version> + <junit-platform-suite.version>1.12.2</junit-platform-suite.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.121.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> @@ -2255,9 +2275,9 @@ <rxjava.version>1.3.8</rxjava.version> <rxjava2.version>2.2.21</rxjava2.version> <simple.version>6.0.1</simple.version> - <slf4j.version>2.0.16</slf4j.version> + <slf4j.version>2.0.17</slf4j.version> <spring6.version>6.0.23</spring6.version> - <testng.version>7.10.2</testng.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 --> @@ -2306,7 +2326,7 @@ <jetty.osgi.version>org.eclipse.jetty.*;version="[11,15)"</jetty.osgi.version> <jetty.version>11.0.24</jetty.version> <jetty.tracing.version>11.0.15</jetty.tracing.version> <!-- special version for tracing support tests, applied before JDK 21--> - <jetty9.version>9.4.55.v20240627</jetty9.version> + <jetty9.version>9.4.57.v20241219</jetty9.version> <jetty.plugin.version>11.0.24</jetty.plugin.version> <jetty.servlet.api.25.version>6.1.14</jetty.servlet.api.25.version> <jsonb.api.version>2.0.0</jsonb.api.version>
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..56e5d71 --- /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 javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.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/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java index db08f92..083fda8 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, 2023 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,34 +16,48 @@ package org.glassfish.jersey.tests.e2e.client.nettyconnector; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AbstractHandler; 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 jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +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.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*/ { @@ -58,38 +72,32 @@ 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 - public static void startExpect100ContinueTestServer() { - server = new Server(portNumber); - server.setHandler(new Expect100ContinueTestHandler()); - try { - server.start(); - } catch (Exception e) { - - } - } - - @AfterAll - public static void stopExpect100ContinueTestServer() { - try { - server.stop(); - } catch (Exception e) { - } - } + private static TestSocketServer server; private static Client client; - @BeforeEach - public void beforeEach() { + + @BeforeAll + static void beforeAll() { final ClientConfig config = new ClientConfig(); - this.configureClient(config); + config.connectorProvider(new NettyConnectorProvider()); client = ClientBuilder.newClient(config); } + @BeforeEach + void beforeEach() throws IOException { + server = new TestSocketServer(portNumber); + server.runServer(); + } + + @AfterEach + void afterEach() { + server.stop(); + } + private Client client() { return client; } @@ -104,137 +112,353 @@ @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 AbstractHandler { - @Override - public void handle(String target, - Request baseRequest, - HttpServletRequest request, - HttpServletResponse response) throws IOException { - boolean expected = request.getHeader("Expect") != null; - boolean failed = false; - if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) { - response.sendError(417); - failed = true; - } - if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) { - response.sendError(401); - failed = true; - } - if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) { - response.sendError(413); - failed = true; - } - if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) { - response.sendError(405); - failed = true; - } - if (expected && !failed) { - System.out.println("Expect:100-continue found, sending response header"); - response.setStatus(204); - } - response.getWriter().println(); - response.flushBuffer(); - baseRequest.setHandled(true); - - request.getReader().lines().forEach(System.out::println); + 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 215cbfc..15a8d13 100644 --- a/tests/e2e-server/pom.xml +++ b/tests/e2e-server/pom.xml
@@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - 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 @@ -210,6 +210,19 @@ </dependency> <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <version>${jetty.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>${commons.io.version}</version> + </dependency> + + <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest</artifactId> <scope>test</scope>
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..8eb3c04 --- /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.apache.commons.io.IOUtils; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.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.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 javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.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 = IOUtils.toByteArray(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/integration/jersey-5796/pom.xml b/tests/integration/jersey-5796/pom.xml index 2710d8e..8ab2985 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.0.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 4f5245f..a6f43f6 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml
@@ -58,6 +58,7 @@ <module>jersey-4542</module> <module>jersey-4697</module> <module>jersey-4722</module> + <module>jersey-5796</module> <module>microprofile</module> <module>reactive-streams</module> <module>jersey-5087</module> @@ -164,6 +165,7 @@ <module>jersey-3796</module> <module>jersey-4949</module> <module>property-check</module> + <module>resteasy-client</module> <module>security-digest</module> <module>servlet-2.5-autodiscovery-1</module> <module>servlet-2.5-autodiscovery-2</module>
diff --git a/tests/integration/resteasy-client/pom.xml b/tests/integration/resteasy-client/pom.xml new file mode 100644 index 0000000..74815a1 --- /dev/null +++ b/tests/integration/resteasy-client/pom.xml
@@ -0,0 +1,104 @@ +<?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 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 + +--> + +<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.tests.integration</groupId> + <artifactId>project</artifactId> + <version>3.0.99-SNAPSHOT</version> + </parent> + + <artifactId>resteasy-client</artifactId> + + <packaging>war</packaging> + <name>jersey-tests-integration-resteasy-client</name> + + <description> + Jersey tests for Spring.Boot / Resteasy integration + </description> + + <dependencies> + <dependency> + <groupId>org.jboss.resteasy</groupId> + <artifactId>resteasy-client</artifactId> + <version>5.0.10.Final</version> <!-- Can use 3.5.16.Final --> + <exclusions> + <exclusion> + <artifactId>commons-logging</artifactId> + <groupId>commons-logging</groupId> + </exclusion> + <exclusion> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework</groupId> + <artifactId>jersey-test-framework-core</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-external</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.test-framework.providers</groupId> + <artifactId>jersey-test-framework-provider-grizzly2</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>commons-logging</groupId> + <artifactId>commons-logging</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + </plugins> + </build> +</project>
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 new file mode 100644 index 0000000..4848388 --- /dev/null +++ b/tests/integration/resteasy-client/src/test/java/org/glassfish/jersey/tests/springboot/RestEasyClientTest.java
@@ -0,0 +1,113 @@ +/* + * 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.springboot; + +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.core.Application; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +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; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +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 { + + private static final CountDownLatch readFromLatch = new CountDownLatch(1); + + @Path("/") + public static class RestEasyClientTestResource { + @POST + @Path("/test") + @Produces(MediaType.APPLICATION_JSON) + public String testPost(String echo) { + return echo; + } + } + + @Override + protected Application configure() { + return new ResourceConfig(RestEasyClientTestResource.class); + } + + @Test + public void test() throws InterruptedException { + 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 { + public TestDefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) { + super(providers, config); + } + + @Override + public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException { + readFromLatch.countDown(); + return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } + } + +}