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