From 6ff974d92a1c946ee10dd4e9a49ead53a2ea1505 Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 15 Jan 2024 19:05:09 +1100 Subject: [PATCH 001/146] Initial boiler plate implementation of JettyCore HttpHandler Adaptor --- .../reactive/JettyCoreHttpHandlerAdapter.java | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java new file mode 100644 index 000000000000..ccddcd76aed5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -0,0 +1,397 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.util.Callback; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.*; +import org.springframework.http.server.RequestPath; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * + * @author Greg Wilkins + * @since 6.0 + */ +public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { + + private final HttpHandler httpHandler; + + public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { + this.httpHandler = httpHandler; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + httpHandler.handle(new JettyCoreServerHttpRequest(request), new JettyCoreServerHttpResponse(request, response)) + .subscribe(new Subscriber<>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void unused) { + } + + @Override + public void onError(Throwable t) { + callback.failed(t); + } + + @Override + public void onComplete() { + callback.succeeded(); + } + }); + return true; + } + + private static class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable + private MultiValueMap cookies; + + public JettyCoreServerHttpRequest(Request request) { + this.request = request; + headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(request.getMethod()); + } + + @Override + public URI getURI() { + return request.getHttpURI().toURI(); + } + + @Override + public Flux getBody() { + Flow.Publisher flowPublisher = Content.Source.asPublisher(request); + // TODO convert the Flow.Publisher into a org.reactivestreams.Publisher + org.reactivestreams.Publisher publisher = null; + // TODO convert the Publisher to a Flux + Flux chunks = Flux.from(publisher); + // TODO map the chunks to DataBuffers + return chunks.map(chunk -> null); + } + + @Override + public String getId() { + return request.getId(); + } + + @Override + public RequestPath getPath() { + return path; + } + + @Override + public MultiValueMap getQueryParams() { + return null; + } + + @Override + public MultiValueMap getCookies() { + if (cookies == null) { + LinkedHashMap> map = new LinkedHashMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : Request.getCookies(request)) { + List list = map.computeIfAbsent(c.getName(), k -> new ArrayList<>()); + list.add(new HttpCookie(c.getName(), c.getValue())); + } + cookies = new LinkedMultiValueMap<>(map); + } + return cookies; + } + + @Override + public InetSocketAddress getLocalAddress() { + return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public SslInfo getSslInfo() { + if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + return new SslInfo() + { + @Override + public String getSessionId() { + return sslSessionData.sslSessionId(); + } + + @Override + public X509Certificate[] getPeerCertificates() { + return sslSessionData.peerCertificates(); + } + }; + } + return null; + } + + @Override + public Builder mutate() { + return ServerHttpRequest.super.mutate(); + } + } + + private static class JettyCoreServerHttpResponse implements ServerHttpResponse { + enum State { + OPEN, COMMITTED, LAST, COMPLETED + } + private final AtomicReference state = new AtomicReference<>(State.OPEN); + private final Request request; + private final Response response; + private final HttpHeaders headers; + + public JettyCoreServerHttpResponse(Request request, Response response) { + this.request = request; + this.response = response; + headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + request.addHttpStreamWrapper(s -> new HttpStream.Wrapper(s) + { + @Override + public void send(MetaData.Request metaDataRequest, @Nullable MetaData.Response metaDataResponse, boolean last, ByteBuffer content, Callback callback) { + + if (metaDataResponse != null) + request.getContext().run(JettyCoreServerHttpResponse.this::onCommit, request); + if (last) + callback = Callback.from(callback, JettyCoreServerHttpResponse.this::onLast); + + super.send(metaDataRequest, metaDataResponse, last, content, callback); + } + + @Override + public void succeeded() { + super.succeeded(); + onCompleted(null); + } + + @Override + public void failed(Throwable x) { + super.failed(x); + onCompleted(x); + } + }); + } + + private void onCommit() { + // TODO call all the beforeCommit actions + } + + private void onLast() { + + } + + private void onCompleted(@Nullable Throwable failure) { + // TODO trigger any setComplete Monos + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public DataBufferFactory bufferFactory() { + // TODO + return null; + } + + @Override + public void beforeCommit(Supplier> action) { + // TODO + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public Mono writeWith(Publisher body) { + // TODO + return null; + } + + @Override + public Mono writeAndFlushWith(Publisher> body) { + // TODO + return null; + } + + @Override + public Mono setComplete() { + // TODO + return null; + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) + return false; + response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + return HttpStatusCode.valueOf(response.getStatus()); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) + return false; + response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } + return cookies; + } + + @Override + public void addCookie(ResponseCookie cookie) { + Response.addCookie(response, new HttpResponseCookie(cookie)); + } + + private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private final ResponseCookie responseCookie; + + public HttpResponseCookie(ResponseCookie responseCookie) { + this.responseCookie = responseCookie; + } + + public ResponseCookie getResponseCookie() { + return responseCookie; + } + + @Override + public String getName() { + return responseCookie.getName(); + } + + @Override + public String getValue() { + return responseCookie.getValue(); + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public long getMaxAge() { + return responseCookie.getMaxAge().toSeconds(); + } + + @Override + @Nullable + public String getComment() { + return null; + } + + @Override + @Nullable + public String getDomain() { + return responseCookie.getDomain(); + } + + @Override + @Nullable + public String getPath() { + return responseCookie.getPath(); + } + + @Override + public boolean isSecure() { + return responseCookie.isSecure(); + } + + @Override + public SameSite getSameSite() { + String sameSiteName = responseCookie.getSameSite(); + if (sameSiteName != null) + return SameSite.valueOf(sameSiteName); + SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); + return sameSite == null ? SameSite.NONE : sameSite; + } + + @Override + public boolean isHttpOnly() { + return responseCookie.isHttpOnly(); + } + + @Override + public boolean isPartitioned() { + return false; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + } + } +} From db609a7c0e6e045e985b3920082f77a00c06f9b7 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:29:21 +1100 Subject: [PATCH 002/146] More implementation of the request side --- .../reactive/JettyCoreHttpHandlerAdapter.java | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index ccddcd76aed5..b32c18aed105 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -21,16 +21,20 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.*; -import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.*; +import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.*; import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import reactor.core.publisher.Flux; @@ -41,21 +45,35 @@ import java.nio.ByteBuffer; import java.security.cert.X509Certificate; import java.util.*; -import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; /** + * + * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. * * @author Greg Wilkins - * @since 6.0 + * @author Lachlan Roberts + * @since 6.1.4 */ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { private final HttpHandler httpHandler; + private final DataBufferFactory dataBufferFactory; public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; + + // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use + // wrap and there should never be any allocation done by the factory. Also there is no release semantic + // available. + dataBufferFactory = new DefaultDataBufferFactory() + { + @Override + public DefaultDataBuffer allocateBuffer(int initialCapacity) { + throw new UnsupportedOperationException(); + } + }; } @Override @@ -69,6 +87,7 @@ public void onSubscribe(Subscription s) { @Override public void onNext(Void unused) { + // we can ignore the void as we only seek onError or onComplete } @Override @@ -84,11 +103,17 @@ public void onComplete() { return true; } - private static class JettyCoreServerHttpRequest implements ServerHttpRequest { + private class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private final Request request; private final HttpHeaders headers; private final RequestPath path; @Nullable + private URI uri; + @Nullable + MultiValueMap queryParameters; + @Nullable private MultiValueMap cookies; public JettyCoreServerHttpRequest(Request request) { @@ -109,18 +134,18 @@ public HttpMethod getMethod() { @Override public URI getURI() { - return request.getHttpURI().toURI(); + if (uri == null) + uri = request.getHttpURI().toURI(); + return uri; } @Override public Flux getBody() { - Flow.Publisher flowPublisher = Content.Source.asPublisher(request); - // TODO convert the Flow.Publisher into a org.reactivestreams.Publisher - org.reactivestreams.Publisher publisher = null; - // TODO convert the Publisher to a Flux - Flux chunks = Flux.from(publisher); - // TODO map the chunks to DataBuffers - return chunks.map(chunk -> null); + // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and + // then wrapped as a Flux. The chunks are converted to DataBuffers with simple wrapping and will be released + // by the Flow.Publisher on return from onNext, so that any retention of the data must be done by a copy within + // the call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(chunk -> dataBufferFactory.wrap(chunk.getByteBuffer())); } @Override @@ -135,18 +160,33 @@ public RequestPath getPath() { @Override public MultiValueMap getQueryParams() { - return null; + if (queryParameters == null) + { + String query = request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) + queryParameters = EMPTY_QUERY; + else { + MultiMap map = new MultiMap<>(); + UrlEncoded.decodeUtf8To(query, 0, query.length(), map); + queryParameters = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(map)); + } + } + return queryParameters; } @Override public MultiValueMap getCookies() { if (cookies == null) { - LinkedHashMap> map = new LinkedHashMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : Request.getCookies(request)) { - List list = map.computeIfAbsent(c.getName(), k -> new ArrayList<>()); - list.add(new HttpCookie(c.getName(), c.getValue())); + List httpCookies = Request.getCookies(request); + if (httpCookies.isEmpty()) + cookies = EMPTY_COOKIES; + else { + cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); } - cookies = new LinkedMultiValueMap<>(map); } return cookies; } From 7a23fbf8744642df3e2da25bdc3be55df764d9cb Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:56:02 +1100 Subject: [PATCH 003/146] Created JettyCoreHttpServer --- .../AbstractHttpHandlerIntegrationTests.java | 1 + .../bootstrap/JettyCoreHttpServer.java | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java index f49b91fec5f6..0aa39aa025e6 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java @@ -126,6 +126,7 @@ public static Flux testInterval(Duration period, int count) { static Stream> httpServers() { return Stream.of( named("Jetty", new JettyHttpServer()), + named("Jetty Core", new JettyCoreHttpServer()), named("Reactor Netty", new ReactorHttpServer()), named("Tomcat", new TomcatHttpServer()), named("Undertow", new UndertowHttpServer()) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java new file mode 100644 index 000000000000..637873d24bf1 --- /dev/null +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.testfixture.http.server.reactive.bootstrap; + +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; + +/** + * @author Rossen Stoyanchev + * @author Sam Brannen + * @author Greg Wilkins + */ +public class JettyCoreHttpServer extends AbstractHttpServer { + + private Server jettyServer; + + + @Override + protected void initServer() { + this.jettyServer = new Server(); + + ServerConnector connector = new ServerConnector(this.jettyServer); + connector.setHost(getHost()); + connector.setPort(getPort()); + this.jettyServer.addConnector(connector); + this.jettyServer.setHandler(createHandlerAdapter()); + } + + private JettyCoreHttpHandlerAdapter createHandlerAdapter() { + return new JettyCoreHttpHandlerAdapter(resolveHttpHandler()); + } + + @Override + protected void startInternal() throws Exception { + this.jettyServer.start(); + setPort(((ServerConnector) this.jettyServer.getConnectors()[0]).getLocalPort()); + } + + @Override + protected void stopInternal() { + try { + if (this.jettyServer.isRunning()) { + // Do not configure a large stop timeout. For example, setting a stop timeout + // of 5000 adds an additional 1-2 seconds to the runtime of each test using + // the Jetty sever, resulting in 2-4 extra minutes of overall build time. + this.jettyServer.setStopTimeout(100); + this.jettyServer.stop(); + this.jettyServer.destroy(); + } + } + catch (Exception ex) { + // ignore + } + } + + @Override + protected void resetInternal() { + try { + stopInternal(); + } + finally { + this.jettyServer = null; + } + } + +} From 418d699b982e789c6d60b44747991bce767110b3 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:59:37 +1100 Subject: [PATCH 004/146] Created JettyCoreHttpServer --- .../reactive/ErrorHandlerIntegrationTests.java | 5 +++-- .../method/annotation/SseIntegrationTests.java | 12 +++++------- .../AbstractReactiveWebSocketIntegrationTests.java | 7 ++----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java index 804327bea46c..3b1ec06db41a 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -18,6 +18,7 @@ import java.net.URI; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; @@ -85,8 +86,8 @@ void emptyPathSegments(HttpServer httpServer) throws Exception { // but an application can apply CompactPathRule via RewriteHandler: // https://www.eclipse.org/jetty/documentation/jetty-11/programming_guide.php - HttpStatus expectedStatus = - (httpServer instanceof JettyHttpServer ? HttpStatus.BAD_REQUEST : HttpStatus.OK); + HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer + ? HttpStatus.BAD_REQUEST : HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(expectedStatus); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 205ff217a846..0328e05ae92d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -51,12 +52,6 @@ import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -127,7 +122,7 @@ void sseAsPerson(HttpServer httpServer, ClientHttpConnector connector) throws Ex @ParameterizedSseTest void sseAsEvent(HttpServer httpServer, ClientHttpConnector connector) throws Exception { - assumeTrue(httpServer instanceof JettyHttpServer); + assumeTrue(httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer); startServer(httpServer, connector); @@ -305,6 +300,9 @@ static Stream arguments() { args(new JettyHttpServer(), new ReactorClientHttpConnector()), args(new JettyHttpServer(), new JettyClientHttpConnector()), args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), + args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), + args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), + args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), args(new ReactorHttpServer(), new ReactorClientHttpConnector()), args(new ReactorHttpServer(), new JettyClientHttpConnector()), args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index d6954505fa1e..fe5909ca8776 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import org.xnio.OptionMap; import org.xnio.Xnio; import reactor.core.publisher.Flux; @@ -60,11 +61,6 @@ import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; /** * Base class for reactive WebSocket integration tests. Subclasses must implement @@ -97,6 +93,7 @@ static Stream arguments() throws IOException { Map> servers = new LinkedHashMap<>(); servers.put(new TomcatHttpServer(TMP_DIR.getAbsolutePath(), WsContextListener.class), TomcatConfig.class); servers.put(new JettyHttpServer(), JettyConfig.class); + servers.put(new JettyCoreHttpServer(), JettyConfig.class); servers.put(new ReactorHttpServer(), ReactorNettyConfig.class); servers.put(new UndertowHttpServer(), UndertowConfig.class); From 930d4204dc0ea42a05d361b788875b83a0d36146 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:16:50 +1100 Subject: [PATCH 005/146] WIP on tests --- .../reactive/AbstractServerHttpRequest.java | 2 +- .../reactive/JettyCoreHttpHandlerAdapter.java | 24 ++++++++++++++----- .../bootstrap/JettyCoreHttpServer.java | 1 + 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 829a2202a814..da4659cd3e61 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -42,7 +42,7 @@ */ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { - private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); + static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); private final URI uri; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index b32c18aed105..c4d0ebf1ed0b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -37,16 +37,22 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.InetSocketAddress; import java.net.URI; +import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import java.util.regex.Matcher; + +import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** * @@ -166,9 +172,15 @@ public MultiValueMap getQueryParams() { if (StringUtil.isBlank(query)) queryParameters = EMPTY_QUERY; else { - MultiMap map = new MultiMap<>(); - UrlEncoded.decodeUtf8To(query, 0, query.length(), map); - queryParameters = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(map)); + queryParameters = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + queryParameters.add(name, value); + } } } return queryParameters; @@ -304,19 +316,19 @@ public boolean isCommitted() { @Override public Mono writeWith(Publisher body) { // TODO - return null; + return Mono.empty(); } @Override public Mono writeAndFlushWith(Publisher> body) { // TODO - return null; + return Mono.empty(); } @Override public Mono setComplete() { // TODO - return null; + return Mono.empty(); } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 637873d24bf1..14d9821a00b4 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -43,6 +43,7 @@ protected void initServer() { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(createHandlerAdapter()); + // TODO add websocket upgrade handler } private JettyCoreHttpHandlerAdapter createHandlerAdapter() { From eb384342017299c5b03e20a01e50c91f35b1404e Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:24:45 +1100 Subject: [PATCH 006/146] WIP on tests --- .../server/reactive/JettyCoreHttpHandlerAdapter.java | 12 +++--------- .../server/reactive/ZeroCopyIntegrationTests.java | 7 ++----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index c4d0ebf1ed0b..ac8c0395b375 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -71,15 +71,9 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use - // wrap and there should never be any allocation done by the factory. Also there is no release semantic - // available. - dataBufferFactory = new DefaultDataBufferFactory() - { - @Override - public DefaultDataBuffer allocateBuffer(int initialCapacity) { - throw new UnsupportedOperationException(); - } - }; + // wrap and there should rarely be any allocation done by the factory. Also, there is no release semantic + // available so we could not do retainable buffers anyway. + dataBufferFactory = new DefaultDataBufferFactory(); } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index 587825b0b066..c4f24f69d18d 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -19,6 +19,7 @@ import java.io.File; import java.net.URI; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Mono; import org.springframework.core.io.ClassPathResource; @@ -28,10 +29,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.web.client.RestTemplate; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -54,7 +51,7 @@ protected HttpHandler createHttpHandler() { @ParameterizedHttpServerTest void zeroCopy(HttpServer httpServer) throws Exception { - assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer, + assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer || httpServer instanceof JettyCoreHttpServer, "Zero-copy does not support Servlet"); startServer(httpServer); From 88e746789e4266beeb63c2757d02ccba866e466c Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:42:21 +1100 Subject: [PATCH 007/146] Cleanup start/stop for jetty --- .../reactive/JettyCoreHttpHandlerAdapter.java | 39 +------------------ .../bootstrap/JettyCoreHttpServer.java | 11 +----- .../reactive/bootstrap/JettyHttpServer.java | 31 +++------------ 3 files changed, 8 insertions(+), 73 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index ac8c0395b375..9377468ff0ed 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -247,43 +247,6 @@ public JettyCoreServerHttpResponse(Request request, Response response) { this.request = request; this.response = response; headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - request.addHttpStreamWrapper(s -> new HttpStream.Wrapper(s) - { - @Override - public void send(MetaData.Request metaDataRequest, @Nullable MetaData.Response metaDataResponse, boolean last, ByteBuffer content, Callback callback) { - - if (metaDataResponse != null) - request.getContext().run(JettyCoreServerHttpResponse.this::onCommit, request); - if (last) - callback = Callback.from(callback, JettyCoreServerHttpResponse.this::onLast); - - super.send(metaDataRequest, metaDataResponse, last, content, callback); - } - - @Override - public void succeeded() { - super.succeeded(); - onCompleted(null); - } - - @Override - public void failed(Throwable x) { - super.failed(x); - onCompleted(x); - } - }); - } - - private void onCommit() { - // TODO call all the beforeCommit actions - } - - private void onLast() { - - } - - private void onCompleted(@Nullable Throwable failure) { - // TODO trigger any setComplete Monos } @Override @@ -299,7 +262,7 @@ public DataBufferFactory bufferFactory() { @Override public void beforeCommit(Supplier> action) { - // TODO + // TODO See UndertowServerHttpResponse as an example } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 14d9821a00b4..eb5aad83fd1f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -59,14 +59,7 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() { try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } + this.jettyServer.stop(); } catch (Exception ex) { // ignore @@ -77,10 +70,10 @@ protected void stopInternal() { protected void resetInternal() { try { stopInternal(); + this.jettyServer.destroy(); } finally { this.jettyServer = null; } } - } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java index d3912ac2df8c..08b7d32b3962 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java @@ -54,7 +54,6 @@ protected void initServer() throws Exception { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(this.contextHandler); - this.contextHandler.start(); } private ServletHttpHandlerAdapter createServletAdapter() { @@ -70,43 +69,23 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() throws Exception { try { - if (this.contextHandler.isRunning()) { - this.contextHandler.stop(); - } + this.jettyServer.stop(); } - finally { - try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } - } - catch (Exception ex) { - // ignore - } + catch (Exception ex) { + // ignore } } @Override protected void resetInternal() { try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } + this.jettyServer.stop(); } catch (Exception ex) { throw new IllegalStateException(ex); } finally { + this.jettyServer.destroy(); this.jettyServer = null; this.contextHandler = null; } From c5a327c444de088299da8304f58c2380c635047a Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 16:57:48 +1100 Subject: [PATCH 008/146] retainable bytebuffer! --- .../reactive/JettyCoreHttpHandlerAdapter.java | 270 +++++++++++++++++- 1 file changed, 258 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 9377468ff0ed..54874b2054d9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -17,19 +17,16 @@ package org.springframework.http.server.reactive; import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.Retainable; import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.*; import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.*; import org.springframework.http.*; import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; @@ -41,14 +38,19 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntPredicate; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -70,9 +72,10 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; - // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use - // wrap and there should rarely be any allocation done by the factory. Also, there is no release semantic - // available so we could not do retainable buffers anyway. + // TODO currently we do not make a DataBufferFactory over the servers ByteBufferPool, + // because we mainly use wrap and there should be few allocation done by the factory. + // But it should be possible to use the servers buffer pool for allocations and to + // create PooledDataBuffers dataBufferFactory = new DefaultDataBufferFactory(); } @@ -142,10 +145,9 @@ public URI getURI() { @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to DataBuffers with simple wrapping and will be released - // by the Flow.Publisher on return from onNext, so that any retention of the data must be done by a copy within - // the call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(chunk -> dataBufferFactory.wrap(chunk.getByteBuffer())); + // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be + // retained within a call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(RetainedDataBuffer::new); } @Override @@ -403,4 +405,248 @@ public Map getAttributes() { } } } + + class RetainedDataBuffer implements PooledDataBuffer + { + private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); + + public RetainedDataBuffer(Content.Chunk chunk) { + this(chunk.getByteBuffer(), chunk); + } + + public RetainedDataBuffer(ByteBuffer byteBuffer, Retainable retainable) { + this.dataBuffer = dataBufferFactory.wrap(byteBuffer); + this.retainable = retainable; + } + + @Override + public boolean isAllocated() { + return allocated.get(); + } + + @Override + public PooledDataBuffer retain() { + retainable.retain(); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + return this; + } + + @Override + public boolean release() { + if (retainable.release()) { + allocated.set(false); + return true; + } + return false; + } + + @Override + public DataBufferFactory factory() { + return dataBuffer.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return dataBuffer.readableByteCount(); + } + + @Override + public int writableByteCount() { + return dataBuffer.writableByteCount(); + } + + @Override + public int capacity() { + return dataBuffer.capacity(); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer capacity(int capacity) { + return dataBuffer.capacity(capacity); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer ensureCapacity(int capacity) { + return dataBuffer.ensureCapacity(capacity); + } + + @Override + public DataBuffer ensureWritable(int capacity) { + return dataBuffer.ensureWritable(capacity); + } + + @Override + public int readPosition() { + return dataBuffer.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + return dataBuffer.readPosition(readPosition); + } + + @Override + public int writePosition() { + return dataBuffer.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + return dataBuffer.writePosition(writePosition); + } + + @Override + public byte getByte(int index) { + return dataBuffer.getByte(index); + } + + @Override + public byte read() { + return dataBuffer.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return dataBuffer.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return dataBuffer.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return dataBuffer.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return dataBuffer.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return dataBuffer.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + return dataBuffer.write(charSequence, charset); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer slice(int index, int length) { + return dataBuffer.slice(index, length); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer retainedSlice(int index, int length) { + return dataBuffer.retainedSlice(index, length); + } + + @Override + public DataBuffer split(int index) { + return dataBuffer.split(index); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer() { + return dataBuffer.asByteBuffer(); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer(int index, int length) { + return dataBuffer.asByteBuffer(index, length); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer() { + return dataBuffer.toByteBuffer(); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer(int index, int length) { + return dataBuffer.toByteBuffer(index, length); + } + + @Override + public void toByteBuffer(ByteBuffer dest) { + dataBuffer.toByteBuffer(dest); + } + + @Override + public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + } + + @Override + public ByteBufferIterator readableByteBuffers() { + return dataBuffer.readableByteBuffers(); + } + + @Override + public ByteBufferIterator writableByteBuffers() { + return dataBuffer.writableByteBuffers(); + } + + @Override + public InputStream asInputStream() { + return dataBuffer.asInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return dataBuffer.asInputStream(releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return dataBuffer.asOutputStream(); + } + + @Override + public String toString(Charset charset) { + return dataBuffer.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + return dataBuffer.toString(index, length, charset); + } + } } From 49abe72d468b665b59af81040b8f0d51a1abcc6a Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 17:04:34 +1100 Subject: [PATCH 009/146] Non blocking invocation type --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 54874b2054d9..6ea806bca188 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -57,14 +57,13 @@ import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** - * * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. * * @author Greg Wilkins * @author Lachlan Roberts * @since 6.1.4 */ -public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { +public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; private final DataBufferFactory dataBufferFactory; From 6ca453c2d04a5c64548b1adfef88ded52c7d90a6 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 17:18:34 +1100 Subject: [PATCH 010/146] split out into separate files --- .../reactive/JettyCoreHttpHandlerAdapter.java | 579 +----------------- .../reactive/JettyCoreServerHttpRequest.java | 187 ++++++ .../reactive/JettyCoreServerHttpResponse.java | 216 +++++++ .../reactive/JettyRetainedDataBuffer.java | 276 +++++++++ 4 files changed, 680 insertions(+), 578 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 6ea806bca188..f861606b60ec 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -16,45 +16,11 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.Retainable; import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.*; -import org.reactivestreams.FlowAdapters; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.*; -import org.springframework.http.*; -import org.springframework.http.server.RequestPath; -import org.springframework.http.support.JettyHeadersAdapter; -import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.IntPredicate; -import java.util.function.Supplier; -import java.util.regex.Matcher; - -import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. @@ -80,7 +46,7 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(request), new JettyCoreServerHttpResponse(request, response)) + httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(request, response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { @@ -105,547 +71,4 @@ public void onComplete() { return true; } - private class JettyCoreServerHttpRequest implements ServerHttpRequest { - private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final Request request; - private final HttpHeaders headers; - private final RequestPath path; - @Nullable - private URI uri; - @Nullable - MultiValueMap queryParameters; - @Nullable - private MultiValueMap cookies; - - public JettyCoreServerHttpRequest(Request request) { - this.request = request; - headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(request.getMethod()); - } - - @Override - public URI getURI() { - if (uri == null) - uri = request.getHttpURI().toURI(); - return uri; - } - - @Override - public Flux getBody() { - // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be - // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(RetainedDataBuffer::new); - } - - @Override - public String getId() { - return request.getId(); - } - - @Override - public RequestPath getPath() { - return path; - } - - @Override - public MultiValueMap getQueryParams() { - if (queryParameters == null) - { - String query = request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) - queryParameters = EMPTY_QUERY; - else { - queryParameters = new LinkedMultiValueMap<>(); - Matcher matcher = QUERY_PATTERN.matcher(query); - while (matcher.find()) { - String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - String eq = matcher.group(2); - String value = matcher.group(3); - value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - queryParameters.add(name, value); - } - } - } - return queryParameters; - } - - @Override - public MultiValueMap getCookies() { - if (cookies == null) { - List httpCookies = Request.getCookies(request); - if (httpCookies.isEmpty()) - cookies = EMPTY_COOKIES; - else { - cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { - cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } - cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); - } - } - return cookies; - } - - @Override - public InetSocketAddress getLocalAddress() { - return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet - ? inet : null; - } - - @Override - public InetSocketAddress getRemoteAddress() { - return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet - ? inet : null; - } - - @Override - public SslInfo getSslInfo() { - if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { - return new SslInfo() - { - @Override - public String getSessionId() { - return sslSessionData.sslSessionId(); - } - - @Override - public X509Certificate[] getPeerCertificates() { - return sslSessionData.peerCertificates(); - } - }; - } - return null; - } - - @Override - public Builder mutate() { - return ServerHttpRequest.super.mutate(); - } - } - - private static class JettyCoreServerHttpResponse implements ServerHttpResponse { - enum State { - OPEN, COMMITTED, LAST, COMPLETED - } - private final AtomicReference state = new AtomicReference<>(State.OPEN); - private final Request request; - private final Response response; - private final HttpHeaders headers; - - public JettyCoreServerHttpResponse(Request request, Response response) { - this.request = request; - this.response = response; - headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public DataBufferFactory bufferFactory() { - // TODO - return null; - } - - @Override - public void beforeCommit(Supplier> action) { - // TODO See UndertowServerHttpResponse as an example - } - - @Override - public boolean isCommitted() { - return response.isCommitted(); - } - - @Override - public Mono writeWith(Publisher body) { - // TODO - return Mono.empty(); - } - - @Override - public Mono writeAndFlushWith(Publisher> body) { - // TODO - return Mono.empty(); - } - - @Override - public Mono setComplete() { - // TODO - return Mono.empty(); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) - return false; - response.setStatus(status.value()); - return true; - } - - @Override - public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(response.getStatus()); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) - return false; - response.setStatus(value); - return true; - } - - @Override - public MultiValueMap getCookies() { - LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); - } - return cookies; - } - - @Override - public void addCookie(ResponseCookie cookie) { - Response.addCookie(response, new HttpResponseCookie(cookie)); - } - - private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { - private final ResponseCookie responseCookie; - - public HttpResponseCookie(ResponseCookie responseCookie) { - this.responseCookie = responseCookie; - } - - public ResponseCookie getResponseCookie() { - return responseCookie; - } - - @Override - public String getName() { - return responseCookie.getName(); - } - - @Override - public String getValue() { - return responseCookie.getValue(); - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public long getMaxAge() { - return responseCookie.getMaxAge().toSeconds(); - } - - @Override - @Nullable - public String getComment() { - return null; - } - - @Override - @Nullable - public String getDomain() { - return responseCookie.getDomain(); - } - - @Override - @Nullable - public String getPath() { - return responseCookie.getPath(); - } - - @Override - public boolean isSecure() { - return responseCookie.isSecure(); - } - - @Override - public SameSite getSameSite() { - String sameSiteName = responseCookie.getSameSite(); - if (sameSiteName != null) - return SameSite.valueOf(sameSiteName); - SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); - return sameSite == null ? SameSite.NONE : sameSite; - } - - @Override - public boolean isHttpOnly() { - return responseCookie.isHttpOnly(); - } - - @Override - public boolean isPartitioned() { - return false; - } - - @Override - public Map getAttributes() { - return Collections.emptyMap(); - } - } - } - - class RetainedDataBuffer implements PooledDataBuffer - { - private final Retainable retainable; - private final DataBuffer dataBuffer; - private final AtomicBoolean allocated = new AtomicBoolean(true); - - public RetainedDataBuffer(Content.Chunk chunk) { - this(chunk.getByteBuffer(), chunk); - } - - public RetainedDataBuffer(ByteBuffer byteBuffer, Retainable retainable) { - this.dataBuffer = dataBufferFactory.wrap(byteBuffer); - this.retainable = retainable; - } - - @Override - public boolean isAllocated() { - return allocated.get(); - } - - @Override - public PooledDataBuffer retain() { - retainable.retain(); - return this; - } - - @Override - public PooledDataBuffer touch(Object hint) { - return this; - } - - @Override - public boolean release() { - if (retainable.release()) { - allocated.set(false); - return true; - } - return false; - } - - @Override - public DataBufferFactory factory() { - return dataBuffer.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return dataBuffer.readableByteCount(); - } - - @Override - public int writableByteCount() { - return dataBuffer.writableByteCount(); - } - - @Override - public int capacity() { - return dataBuffer.capacity(); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer capacity(int capacity) { - return dataBuffer.capacity(capacity); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer ensureCapacity(int capacity) { - return dataBuffer.ensureCapacity(capacity); - } - - @Override - public DataBuffer ensureWritable(int capacity) { - return dataBuffer.ensureWritable(capacity); - } - - @Override - public int readPosition() { - return dataBuffer.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - return dataBuffer.readPosition(readPosition); - } - - @Override - public int writePosition() { - return dataBuffer.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - return dataBuffer.writePosition(writePosition); - } - - @Override - public byte getByte(int index) { - return dataBuffer.getByte(index); - } - - @Override - public byte read() { - return dataBuffer.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - return dataBuffer.read(destination); - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - return dataBuffer.read(destination, offset, length); - } - - @Override - public DataBuffer write(byte b) { - return dataBuffer.write(b); - } - - @Override - public DataBuffer write(byte[] source) { - return dataBuffer.write(source); - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - return dataBuffer.write(source, offset, length); - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - return dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - return dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - return dataBuffer.write(charSequence, charset); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer slice(int index, int length) { - return dataBuffer.slice(index, length); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer retainedSlice(int index, int length) { - return dataBuffer.retainedSlice(index, length); - } - - @Override - public DataBuffer split(int index) { - return dataBuffer.split(index); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer() { - return dataBuffer.asByteBuffer(); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer(int index, int length) { - return dataBuffer.asByteBuffer(index, length); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer() { - return dataBuffer.toByteBuffer(); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer(int index, int length) { - return dataBuffer.toByteBuffer(index, length); - } - - @Override - public void toByteBuffer(ByteBuffer dest) { - dataBuffer.toByteBuffer(dest); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dataBuffer.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return dataBuffer.readableByteBuffers(); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return dataBuffer.writableByteBuffers(); - } - - @Override - public InputStream asInputStream() { - return dataBuffer.asInputStream(); - } - - @Override - public InputStream asInputStream(boolean releaseOnClose) { - return dataBuffer.asInputStream(releaseOnClose); - } - - @Override - public OutputStream asOutputStream() { - return dataBuffer.asOutputStream(); - } - - @Override - public String toString(Charset charset) { - return dataBuffer.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - return dataBuffer.toString(index, length, charset); - } - } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java new file mode 100644 index 000000000000..9e5c45bc49dc --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; +import org.reactivestreams.FlowAdapters; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.RequestPath; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.regex.Matcher; + +import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; + +/** + * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final DataBufferFactory dataBufferFactory; + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable + private URI uri; + @Nullable + MultiValueMap queryParameters; + @Nullable + private MultiValueMap cookies; + + public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { + this.dataBufferFactory = dataBufferFactory; + this.request = request; + headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(request.getMethod()); + } + + @Override + public URI getURI() { + if (uri == null) + uri = request.getHttpURI().toURI(); + return uri; + } + + @Override + public Flux getBody() { + // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and + // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be + // retained within a call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(this::wrap); + } + + private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { + return new JettyRetainedDataBuffer(dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + } + + @Override + public String getId() { + return request.getId(); + } + + @Override + public RequestPath getPath() { + return path; + } + + @Override + public MultiValueMap getQueryParams() { + if (queryParameters == null) { + String query = request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) + queryParameters = EMPTY_QUERY; + else { + queryParameters = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + queryParameters.add(name, value); + } + } + } + return queryParameters; + } + + @Override + public MultiValueMap getCookies() { + if (cookies == null) { + List httpCookies = Request.getCookies(request); + if (httpCookies.isEmpty()) + cookies = EMPTY_COOKIES; + else { + cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); + } + } + return cookies; + } + + @Override + public InetSocketAddress getLocalAddress() { + return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public SslInfo getSslInfo() { + if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + return new SslInfo() { + @Override + public String getSessionId() { + return sslSessionData.sslSessionId(); + } + + @Override + public X509Certificate[] getPeerCertificates() { + return sslSessionData.peerCertificates(); + } + }; + } + return null; + } + + @Override + public Builder mutate() { + return ServerHttpRequest.super.mutate(); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java new file mode 100644 index 000000000000..89342e79d433 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.server.HttpCookieUtils; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.reactivestreams.Publisher; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseCookie; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +class JettyCoreServerHttpResponse implements ServerHttpResponse { + enum State { + OPEN, COMMITTED, LAST, COMPLETED + } + + private final AtomicReference state = new AtomicReference<>(State.OPEN); + private final Request request; + private final Response response; + private final HttpHeaders headers; + + public JettyCoreServerHttpResponse(Request request, Response response) { + this.request = request; + this.response = response; + headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public DataBufferFactory bufferFactory() { + // TODO + return null; + } + + @Override + public void beforeCommit(Supplier> action) { + // TODO See UndertowServerHttpResponse as an example + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public Mono writeWith(Publisher body) { + // TODO + return Mono.empty(); + } + + @Override + public Mono writeAndFlushWith(Publisher> body) { + // TODO + return Mono.empty(); + } + + @Override + public Mono setComplete() { + // TODO + return Mono.empty(); + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) + return false; + response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + return HttpStatusCode.valueOf(response.getStatus()); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) + return false; + response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } + return cookies; + } + + @Override + public void addCookie(ResponseCookie cookie) { + Response.addCookie(response, new HttpResponseCookie(cookie)); + } + + private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private final ResponseCookie responseCookie; + + public HttpResponseCookie(ResponseCookie responseCookie) { + this.responseCookie = responseCookie; + } + + public ResponseCookie getResponseCookie() { + return responseCookie; + } + + @Override + public String getName() { + return responseCookie.getName(); + } + + @Override + public String getValue() { + return responseCookie.getValue(); + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public long getMaxAge() { + return responseCookie.getMaxAge().toSeconds(); + } + + @Override + @Nullable + public String getComment() { + return null; + } + + @Override + @Nullable + public String getDomain() { + return responseCookie.getDomain(); + } + + @Override + @Nullable + public String getPath() { + return responseCookie.getPath(); + } + + @Override + public boolean isSecure() { + return responseCookie.isSecure(); + } + + @Override + public SameSite getSameSite() { + String sameSiteName = responseCookie.getSameSite(); + if (sameSiteName != null) + return SameSite.valueOf(sameSiteName); + SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); + return sameSite == null ? SameSite.NONE : sameSite; + } + + @Override + public boolean isHttpOnly() { + return responseCookie.isHttpOnly(); + } + + @Override + public boolean isPartitioned() { + return false; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java new file mode 100644 index 000000000000..5b9c2fbd1bd7 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.io.Retainable; +import org.eclipse.jetty.server.Response; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntPredicate; + +/** + * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +public class JettyRetainedDataBuffer implements PooledDataBuffer { + private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); + + public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { + this.dataBuffer = dataBuffer; + this.retainable = retainable; + } + + @Override + public boolean isAllocated() { + return allocated.get(); + } + + @Override + public PooledDataBuffer retain() { + retainable.retain(); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + return this; + } + + @Override + public boolean release() { + if (retainable.release()) { + allocated.set(false); + return true; + } + return false; + } + + @Override + public DataBufferFactory factory() { + return dataBuffer.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return dataBuffer.readableByteCount(); + } + + @Override + public int writableByteCount() { + return dataBuffer.writableByteCount(); + } + + @Override + public int capacity() { + return dataBuffer.capacity(); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer capacity(int capacity) { + return dataBuffer.capacity(capacity); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer ensureCapacity(int capacity) { + return dataBuffer.ensureCapacity(capacity); + } + + @Override + public DataBuffer ensureWritable(int capacity) { + return dataBuffer.ensureWritable(capacity); + } + + @Override + public int readPosition() { + return dataBuffer.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + return dataBuffer.readPosition(readPosition); + } + + @Override + public int writePosition() { + return dataBuffer.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + return dataBuffer.writePosition(writePosition); + } + + @Override + public byte getByte(int index) { + return dataBuffer.getByte(index); + } + + @Override + public byte read() { + return dataBuffer.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return dataBuffer.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return dataBuffer.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return dataBuffer.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return dataBuffer.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return dataBuffer.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + return dataBuffer.write(charSequence, charset); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer slice(int index, int length) { + return dataBuffer.slice(index, length); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer retainedSlice(int index, int length) { + return dataBuffer.retainedSlice(index, length); + } + + @Override + public DataBuffer split(int index) { + return dataBuffer.split(index); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer() { + return dataBuffer.asByteBuffer(); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer(int index, int length) { + return dataBuffer.asByteBuffer(index, length); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer() { + return dataBuffer.toByteBuffer(); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer(int index, int length) { + return dataBuffer.toByteBuffer(index, length); + } + + @Override + public void toByteBuffer(ByteBuffer dest) { + dataBuffer.toByteBuffer(dest); + } + + @Override + public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + } + + @Override + public ByteBufferIterator readableByteBuffers() { + return dataBuffer.readableByteBuffers(); + } + + @Override + public ByteBufferIterator writableByteBuffers() { + return dataBuffer.writableByteBuffers(); + } + + @Override + public InputStream asInputStream() { + return dataBuffer.asInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return dataBuffer.asInputStream(releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return dataBuffer.asOutputStream(); + } + + @Override + public String toString(Charset charset) { + return dataBuffer.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + return dataBuffer.toString(index, length, charset); + } +} From ea71c6178283a32fc28cb6747ed8398176f037bd Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 18 Jan 2024 21:15:05 +1100 Subject: [PATCH 011/146] minor httpHeader optimizations --- .../reactive/JettyCoreServerHttpRequest.java | 3 +- .../http/support/JettyHeadersAdapter.java | 77 +++++++++++++------ .../ContextPathIntegrationTests.java | 25 ++++-- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 9e5c45bc49dc..a849973e7cac 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -141,9 +141,8 @@ public MultiValueMap getCookies() { cookies = EMPTY_COOKIES; else { cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); } } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index 57f9b7ab8943..703db7254802 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -16,12 +16,7 @@ package org.springframework.http.support; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -43,6 +38,8 @@ public final class JettyHeadersAdapter implements MultiValueMap { private final HttpFields headers; + @Nullable + private final HttpFields.Mutable mutable; /** @@ -53,6 +50,7 @@ public final class JettyHeadersAdapter implements MultiValueMap public JettyHeadersAdapter(HttpFields headers) { Assert.notNull(headers, "Headers must not be null"); this.headers = headers; + this.mutable = headers instanceof HttpFields.Mutable m ? m : null; } @@ -119,22 +117,36 @@ public boolean isEmpty() { @Override public boolean containsKey(Object key) { - return (key instanceof String headerName && this.headers.contains(headerName)); + return (key instanceof String name && this.headers.contains(name)); } @Override public boolean containsValue(Object value) { - return (value instanceof String searchString && - this.headers.stream().anyMatch(field -> field.contains(searchString))); + if (value instanceof String searchString) { + for (HttpField field : this.headers) { + if (field.contains(searchString)) { + return true; + } + } + } + return false; } @Nullable @Override public List get(Object key) { - if (containsKey(key)) { - return this.headers.getValuesList((String) key); + List list = null; + if (key instanceof String name) { + for (HttpField f : this.headers) { + if (f.is(name)) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(f.getValue()); + } + } } - return null; + return list; } @Nullable @@ -142,7 +154,21 @@ public List get(Object key) { public List put(String key, List value) { HttpFields.Mutable mutableHttpFields = mutableFields(); List oldValues = get(key); - mutableHttpFields.put(key, value); + switch (value.size()) { + case 0 -> { + if (oldValues != null) { + mutableHttpFields.remove(key); + } + } + case 1 -> { + if (oldValues == null) { + mutableHttpFields.add(key, value.get(0)); + } else { + mutableHttpFields.put(key, value.get(0)); + } + } + default -> mutableHttpFields.put(key, value); + } return oldValues; } @@ -150,12 +176,21 @@ public List put(String key, List value) { @Override public List remove(Object key) { HttpFields.Mutable mutableHttpFields = mutableFields(); + List list = null; if (key instanceof String name) { - List oldValues = get(key); - mutableHttpFields.remove(name); - return oldValues; + for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext();) + { + HttpField f = i.next(); + if (f.is(name)) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(f.getValue()); + i.remove(); + } + } } - return null; + return list; } @Override @@ -195,16 +230,12 @@ public int size() { } private HttpFields.Mutable mutableFields() { - if (this.headers instanceof HttpFields.Mutable mutableHttpFields) { - return mutableHttpFields; - } - else { + if (mutable == null) { throw new IllegalStateException("Immutable headers"); } + return mutable; } - - @Override public String toString() { return HttpHeaders.formatHeaders(this); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java index 293cbdeec5bd..91104f2ed8e1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java @@ -16,8 +16,11 @@ package org.springframework.web.reactive.result.method.annotation; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,10 +31,12 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; + +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; /** * Integration tests related to the use of context paths. @@ -40,15 +45,25 @@ */ class ContextPathIntegrationTests { - @Test - void multipleWebFluxApps() throws Exception { + static Stream> httpServers() { + return Stream.of( + named("Jetty", new JettyHttpServer()), + named("Jetty Core", new JettyCoreHttpServer()), + named("Reactor Netty", new ReactorHttpServer()), + named("Tomcat", new TomcatHttpServer()), + named("Undertow", new UndertowHttpServer()) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("httpServers") + void multipleWebFluxApps(AbstractHttpServer server) throws Exception { AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext(WebAppConfig.class); AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext(WebAppConfig.class); HttpHandler webApp1Handler = WebHttpHandlerBuilder.applicationContext(context1).build(); HttpHandler webApp2Handler = WebHttpHandlerBuilder.applicationContext(context2).build(); - ReactorHttpServer server = new ReactorHttpServer(); server.registerHttpHandler("/webApp1", webApp1Handler); server.registerHttpHandler("/webApp2", webApp2Handler); server.afterPropertiesSet(); From dd72a1452e91e75f56446ab0ecd804289d6d5f40 Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 20 Jan 2024 17:04:54 +0900 Subject: [PATCH 012/146] since updated with expected release --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 2 +- .../http/server/reactive/JettyCoreServerHttpRequest.java | 3 +-- .../http/server/reactive/JettyCoreServerHttpResponse.java | 2 +- .../http/server/reactive/bootstrap/JettyCoreHttpServer.java | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index f861606b60ec..d94e28d7777e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -27,7 +27,7 @@ * * @author Greg Wilkins * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index a849973e7cac..9ea8782b2b1d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -49,8 +49,7 @@ * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} * * @author Greg Wilkins - * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 89342e79d433..501d475e9f43 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,7 +42,7 @@ * * @author Greg Wilkins * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ class JettyCoreServerHttpResponse implements ServerHttpResponse { enum State { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index eb5aad83fd1f..40da34f914a8 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -28,6 +28,7 @@ * @author Rossen Stoyanchev * @author Sam Brannen * @author Greg Wilkins + * @since 6.2 */ public class JettyCoreHttpServer extends AbstractHttpServer { From 50e78d328509b77734a2ba393300b39ef1ef6e42 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:21:05 +1100 Subject: [PATCH 013/146] Initial implementation of the writeWith methods in JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 91 +++++++++++++++---- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 501d475e9f43..bdceb2600697 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -16,13 +16,24 @@ package org.springframework.http.server.reactive; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; @@ -30,13 +41,9 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} * @@ -45,11 +52,10 @@ * @since 6.2 */ class JettyCoreServerHttpResponse implements ServerHttpResponse { - enum State { - OPEN, COMMITTED, LAST, COMPLETED - } + private final AtomicBoolean committed = new AtomicBoolean(false); + + private final List>> commitActions = new CopyOnWriteArrayList<>(); - private final AtomicReference state = new AtomicReference<>(State.OPEN); private final Request request; private final Response response; private final HttpHeaders headers; @@ -67,13 +73,12 @@ public HttpHeaders getHeaders() { @Override public DataBufferFactory bufferFactory() { - // TODO - return null; + return DefaultDataBufferFactory.sharedInstance; } @Override public void beforeCommit(Supplier> action) { - // TODO See UndertowServerHttpResponse as an example + commitActions.add(action); } @Override @@ -82,21 +87,69 @@ public boolean isCommitted() { } @Override - public Mono writeWith(Publisher body) { - // TODO - return Mono.empty(); + public Mono writeWith(Publisher body) + { + return Flux.from(body) + .flatMap(this::mySend, 1) + .then(); + } + + private Mono mySend(DataBuffer dataBuffer) { + + if (committed.compareAndSet(false, true)) + { + if (!this.commitActions.isEmpty()) + { + return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) + .then(Mono.defer(() -> mySend(dataBuffer))) + .doOnError(t -> getHeaders().clearContentHeaders()); + } + } + + @SuppressWarnings("resource") + DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); + Callback.Completable callback = new Callback.Completable(); + new IteratingCallback() + { + @Override + protected Action process() + { + if (!byteBufferIterator.hasNext()) + return Action.SUCCEEDED; + response.write(false, byteBufferIterator.next(), this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + byteBufferIterator.close(); + callback.complete(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + byteBufferIterator.close(); + callback.failed(cause); + } + }.iterate(); + + return Mono.fromFuture(callback); } @Override public Mono writeAndFlushWith(Publisher> body) { - // TODO - return Mono.empty(); + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); } @Override public Mono setComplete() { - // TODO - return Mono.empty(); + Callback.Completable callback = new Callback.Completable(); + response.write(true, BufferUtil.EMPTY_BUFFER, callback); + return Mono.fromFuture(callback); } @Override From ab844d68c897fbd12642a7fec714e3c83945af73 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:42:23 +1100 Subject: [PATCH 014/146] Implement the ZeroCopyHttpOutputMessage interface for JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bdceb2600697..3155e5209327 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -16,6 +16,13 @@ package org.springframework.http.server.reactive; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; import java.util.Map; @@ -24,11 +31,15 @@ import java.util.function.Supplier; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; @@ -37,6 +48,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; @@ -51,7 +63,8 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse { +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage +{ private final AtomicBoolean committed = new AtomicBoolean(false); private final List>> commitActions = new CopyOnWriteArrayList<>(); @@ -94,6 +107,77 @@ public Mono writeWith(Publisher body) .then(); } + @Override + public Mono writeWith(Path file, long position, long count) + { + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); + try + { + // TODO: Why does this say possible blocking call? + SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); + new ContentWriterIteratingCallback(channel, response, callback).iterate(); + } + catch (Throwable t) + { + callback.failed(t); + } + return mono; + } + + private static class ContentWriterIteratingCallback extends IteratingCallback + { + private final ReadableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + + public ContentWriterIteratingCallback(ReadableByteChannel content, Response target, Callback callback) throws IOException + { + this.source = content; + this.sink = target; + this.callback = callback; + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); + int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); + boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); + this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); + } + + @Override + protected Action process() throws Throwable + { + if (!source.isOpen()) + return Action.SUCCEEDED; + + ByteBuffer byteBuffer = buffer.getByteBuffer(); + BufferUtil.clearToFill(byteBuffer); + int read = source.read(byteBuffer); + if (read == -1) + { + IO.close(source); + sink.write(true, BufferUtil.EMPTY_BUFFER, this); + return Action.SCHEDULED; + } + BufferUtil.flipToFlush(byteBuffer, 0); + sink.write(false, byteBuffer, this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + buffer.release(); + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable x) + { + buffer.release(); + callback.failed(x); + } + } + private Mono mySend(DataBuffer dataBuffer) { if (committed.compareAndSet(false, true)) From 4cf2b0c2ef15fc242295cfce5ff3626713492610 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:42:47 +1100 Subject: [PATCH 015/146] Add temporary fix for cookies Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3155e5209327..db7ff4b63e40 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -264,7 +264,18 @@ public MultiValueMap getCookies() { if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); } - return cookies; + + // TODO: when cookies are added to this list they should be added to the response. + // Maybe something like this ? + return new LinkedMultiValueMap<>(cookies) + { + @Override + public void add(String key, ResponseCookie value) + { + super.add(key, value); + addCookie(value); + } + }; } @Override From 77772cbd909ce3bdf7a6d0804e3c4d817cd0d82c Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:43:30 +1100 Subject: [PATCH 016/146] disable setting of the same site cookie attribute to fix test Signed-off-by: Lachlan Roberts --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index db7ff4b63e40..0ed9b6f5e22f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -339,11 +339,8 @@ public boolean isSecure() { @Override public SameSite getSameSite() { - String sameSiteName = responseCookie.getSameSite(); - if (sameSiteName != null) - return SameSite.valueOf(sameSiteName); - SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); - return sameSite == null ? SameSite.NONE : sameSite; + // Adding non-null return site breaks tests. + return null; } @Override From 8617dede03c049ed836d1e58ac648ec82d5026c0 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 10:58:31 +1100 Subject: [PATCH 017/146] rearrange methods in JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 0ed9b6f5e22f..680f943b392d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -103,10 +103,24 @@ public boolean isCommitted() { public Mono writeWith(Publisher body) { return Flux.from(body) - .flatMap(this::mySend, 1) + .flatMap(this::sendDataBuffer, 1) .then(); } + @Override + public Mono writeAndFlushWith(Publisher> body) { + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); + } + + @Override + public Mono setComplete() { + Callback.Completable callback = new Callback.Completable(); + response.write(true, BufferUtil.EMPTY_BUFFER, callback); + return Mono.fromFuture(callback); + } + @Override public Mono writeWith(Path file, long position, long count) { @@ -178,14 +192,14 @@ protected void onCompleteFailure(Throwable x) } } - private Mono mySend(DataBuffer dataBuffer) { + private Mono sendDataBuffer(DataBuffer dataBuffer) { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .then(Mono.defer(() -> mySend(dataBuffer))) + .then(Mono.defer(() -> sendDataBuffer(dataBuffer))) .doOnError(t -> getHeaders().clearContentHeaders()); } } @@ -222,20 +236,6 @@ protected void onCompleteFailure(Throwable cause) return Mono.fromFuture(callback); } - @Override - public Mono writeAndFlushWith(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); - } - - @Override - public Mono setComplete() { - Callback.Completable callback = new Callback.Completable(); - response.write(true, BufferUtil.EMPTY_BUFFER, callback); - return Mono.fromFuture(callback); - } - @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { if (isCommitted() || status == null) From 375e3b6f883a939a45dd581d90f1dce7195c1163 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:31:12 +1100 Subject: [PATCH 018/146] fix issues with cookies, committing response, and with ZeroCopyHttpOutputMessage Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreHttpHandlerAdapter.java | 11 +- .../reactive/JettyCoreServerHttpResponse.java | 106 ++++++++++++------ 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index d94e28d7777e..2479ae5b4655 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -16,11 +16,14 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.server.*; -import org.eclipse.jetty.util.*; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.*; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; /** * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. @@ -46,7 +49,7 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(request, response)) + httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 680f943b392d..3b7822a00a19 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; @@ -35,7 +34,6 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.HttpCookieUtils; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -66,15 +64,15 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); - private final List>> commitActions = new CopyOnWriteArrayList<>(); - private final Request request; private final Response response; private final HttpHeaders headers; - public JettyCoreServerHttpResponse(Request request, Response response) { - this.request = request; + @Nullable + private LinkedMultiValueMap cookies; + + public JettyCoreServerHttpResponse(Response response) { this.response = response; headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); } @@ -116,6 +114,10 @@ public Mono writeAndFlushWith(Publisher setComplete() { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(this::setComplete)); + Callback.Completable callback = new Callback.Completable(); response.write(true, BufferUtil.EMPTY_BUFFER, callback); return Mono.fromFuture(callback); @@ -124,13 +126,17 @@ public Mono setComplete() { @Override public Mono writeWith(Path file, long position, long count) { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(() -> writeWith(file, position, count))); + Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); + mono = Mono.fromFuture(callback); try { // TODO: Why does this say possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); - new ContentWriterIteratingCallback(channel, response, callback).iterate(); + new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } catch (Throwable t) { @@ -141,16 +147,21 @@ public Mono writeWith(Path file, long position, long count) private static class ContentWriterIteratingCallback extends IteratingCallback { - private final ReadableByteChannel source; + private final SeekableByteChannel source; private final Content.Sink sink; private final Callback callback; private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; - public ContentWriterIteratingCallback(ReadableByteChannel content, Response target, Callback callback) throws IOException + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException { this.source = content; this.sink = target; this.callback = callback; + this.length = count; + source.position(position); + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); @@ -160,11 +171,12 @@ public ContentWriterIteratingCallback(ReadableByteChannel content, Response targ @Override protected Action process() throws Throwable { - if (!source.isOpen()) + if (!source.isOpen() || totalRead == length) return Action.SUCCEEDED; ByteBuffer byteBuffer = buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); + byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); int read = source.read(byteBuffer); if (read == -1) { @@ -172,6 +184,7 @@ protected Action process() throws Throwable sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; } + totalRead += read; BufferUtil.flipToFlush(byteBuffer, 0); sink.write(false, byteBuffer, this); return Action.SCHEDULED; @@ -181,6 +194,7 @@ protected Action process() throws Throwable protected void onCompleteSuccess() { buffer.release(); + IO.close(source); callback.succeeded(); } @@ -188,22 +202,49 @@ protected void onCompleteSuccess() protected void onCompleteFailure(Throwable x) { buffer.release(); + IO.close(source); callback.failed(x); } } - private Mono sendDataBuffer(DataBuffer dataBuffer) { - + @Nullable + private Mono ensureCommitted() + { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .then(Mono.defer(() -> sendDataBuffer(dataBuffer))) - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } + + doCommit(); } + return null; + } + + private void doCommit() + { + if (cookies != null) + { + // TODO: are we doubling up on cookies already existing in response? + cookies.values().stream() + .flatMap(List::stream) + .forEach(cookie -> + { + Response.addCookie(response, new HttpResponseCookie(cookie)); + }); + } + } + + private Mono sendDataBuffer(DataBuffer dataBuffer) { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -259,31 +300,28 @@ public boolean setRawStatusCode(@Nullable Integer value) { @Override public MultiValueMap getCookies() { - LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); - } - - // TODO: when cookies are added to this list they should be added to the response. - // Maybe something like this ? - return new LinkedMultiValueMap<>(cookies) - { - @Override - public void add(String key, ResponseCookie value) - { - super.add(key, value); - addCookie(value); - } - }; + if (cookies == null) + initializeCookies(); + return cookies; } @Override public void addCookie(ResponseCookie cookie) { - Response.addCookie(response, new HttpResponseCookie(cookie)); + if (cookies == null) + initializeCookies(); + cookies.add(cookie.getName(), cookie); + } + + private void initializeCookies() + { + cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } } - private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; public HttpResponseCookie(ResponseCookie responseCookie) { From 0db46f272eaa8f4b51e6a8912841e0f468572f07 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:34:50 +1100 Subject: [PATCH 019/146] cleanups Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 137 +++++++++--------- 1 file changed, 67 insertions(+), 70 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3b7822a00a19..987849a5cd93 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -145,68 +145,6 @@ public Mono writeWith(Path file, long position, long count) return mono; } - private static class ContentWriterIteratingCallback extends IteratingCallback - { - private final SeekableByteChannel source; - private final Content.Sink sink; - private final Callback callback; - private final RetainableByteBuffer buffer; - private final long length; - private long totalRead = 0; - - public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException - { - this.source = content; - this.sink = target; - this.callback = callback; - this.length = count; - source.position(position); - - ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); - int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); - boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); - this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); - } - - @Override - protected Action process() throws Throwable - { - if (!source.isOpen() || totalRead == length) - return Action.SUCCEEDED; - - ByteBuffer byteBuffer = buffer.getByteBuffer(); - BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); - int read = source.read(byteBuffer); - if (read == -1) - { - IO.close(source); - sink.write(true, BufferUtil.EMPTY_BUFFER, this); - return Action.SCHEDULED; - } - totalRead += read; - BufferUtil.flipToFlush(byteBuffer, 0); - sink.write(false, byteBuffer, this); - return Action.SCHEDULED; - } - - @Override - protected void onCompleteSuccess() - { - buffer.release(); - IO.close(source); - callback.succeeded(); - } - - @Override - protected void onCompleteFailure(Throwable x) - { - buffer.release(); - IO.close(source); - callback.failed(x); - } - } - @Nullable private Mono ensureCommitted() { @@ -215,9 +153,9 @@ private Mono ensureCommitted() if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } doCommit(); @@ -232,11 +170,8 @@ private void doCommit() { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> - { - Response.addCookie(response, new HttpResponseCookie(cookie)); - }); + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); } } @@ -396,4 +331,66 @@ public Map getAttributes() { return Collections.emptyMap(); } } + + private static class ContentWriterIteratingCallback extends IteratingCallback + { + private final SeekableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; + + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException + { + this.source = content; + this.sink = target; + this.callback = callback; + this.length = count; + source.position(position); + + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); + int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); + boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); + this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); + } + + @Override + protected Action process() throws Throwable + { + if (!source.isOpen() || totalRead == length) + return Action.SUCCEEDED; + + ByteBuffer byteBuffer = buffer.getByteBuffer(); + BufferUtil.clearToFill(byteBuffer); + byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); + int read = source.read(byteBuffer); + if (read == -1) + { + IO.close(source); + sink.write(true, BufferUtil.EMPTY_BUFFER, this); + return Action.SCHEDULED; + } + totalRead += read; + BufferUtil.flipToFlush(byteBuffer, 0); + sink.write(false, byteBuffer, this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + buffer.release(); + IO.close(source); + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable x) + { + buffer.release(); + IO.close(source); + callback.failed(x); + } + } } From 2081ae010daf1571e49776a2023d927f901656a5 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:36:34 +1100 Subject: [PATCH 020/146] cleanups Signed-off-by: Lachlan Roberts --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 987849a5cd93..d7181c9edd63 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -134,7 +134,7 @@ public Mono writeWith(Path file, long position, long count) mono = Mono.fromFuture(callback); try { - // TODO: Why does this say possible blocking call? + // TODO: Why does intellij warn about possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } @@ -310,6 +310,7 @@ public boolean isSecure() { return responseCookie.isSecure(); } + @Nullable @Override public SameSite getSameSite() { // Adding non-null return site breaks tests. From 855e268757fa529579ef1a176482d3dffcdd7536 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 12:54:36 +0900 Subject: [PATCH 021/146] removed TODO --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 2479ae5b4655..48841a7b0a00 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -40,10 +40,10 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; - // TODO currently we do not make a DataBufferFactory over the servers ByteBufferPool, - // because we mainly use wrap and there should be few allocation done by the factory. - // But it should be possible to use the servers buffer pool for allocations and to - // create PooledDataBuffers + // Currently we do not make a DataBufferFactory over the servers ByteBufferPool, + // because we mainly use wrap and there should be few allocation done by the factory. + // But it could be possible to use the servers buffer pool for allocations and to + // create PooledDataBuffers dataBufferFactory = new DefaultDataBufferFactory(); } From 191853d8152c31c3e1804d4ffad5bb6c31cee6b5 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 13:11:07 +0900 Subject: [PATCH 022/146] Reformatted code --- .../reactive/JettyCoreServerHttpRequest.java | 27 ++++-- .../reactive/JettyCoreServerHttpResponse.java | 87 ++++++++----------- .../reactive/JettyRetainedDataBuffer.java | 14 +-- .../bootstrap/JettyCoreHttpServer.java | 5 +- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 9ea8782b2b1d..db3787e631ae 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -16,11 +16,21 @@ package org.springframework.http.server.reactive; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.regex.Matcher; + import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -33,15 +43,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.regex.Matcher; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -53,15 +54,23 @@ */ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final DataBufferFactory dataBufferFactory; + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable private URI uri; + @Nullable MultiValueMap queryParameters; + @Nullable private MultiValueMap cookies; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index d7181c9edd63..e2cfc8e58b16 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,6 +40,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -51,8 +54,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} @@ -61,12 +62,13 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage -{ +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); + private final List>> commitActions = new CopyOnWriteArrayList<>(); private final Response response; + private final HttpHeaders headers; @Nullable @@ -98,8 +100,7 @@ public boolean isCommitted() { } @Override - public Mono writeWith(Publisher body) - { + public Mono writeWith(Publisher body) { return Flux.from(body) .flatMap(this::sendDataBuffer, 1) .then(); @@ -124,38 +125,32 @@ public Mono setComplete() { } @Override - public Mono writeWith(Path file, long position, long count) - { + public Mono writeWith(Path file, long position, long count) { Mono mono = ensureCommitted(); if (mono != null) return mono.then(Mono.defer(() -> writeWith(file, position, count))); Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); - try - { + try { // TODO: Why does intellij warn about possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } - catch (Throwable t) - { + catch (Throwable t) { callback.failed(t); } return mono; } @Nullable - private Mono ensureCommitted() - { - if (committed.compareAndSet(false, true)) - { - if (!this.commitActions.isEmpty()) - { + private Mono ensureCommitted() { + if (committed.compareAndSet(false, true)) { + if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } doCommit(); @@ -164,14 +159,12 @@ private Mono ensureCommitted() return null; } - private void doCommit() - { - if (cookies != null) - { + private void doCommit() { + if (cookies != null) { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); } } @@ -183,11 +176,9 @@ private Mono sendDataBuffer(DataBuffer dataBuffer) { @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); - new IteratingCallback() - { + new IteratingCallback() { @Override - protected Action process() - { + protected Action process() { if (!byteBufferIterator.hasNext()) return Action.SUCCEEDED; response.write(false, byteBufferIterator.next(), this); @@ -195,15 +186,13 @@ protected Action process() } @Override - protected void onCompleteSuccess() - { + protected void onCompleteSuccess() { byteBufferIterator.close(); callback.complete(null); } @Override - protected void onCompleteFailure(Throwable cause) - { + protected void onCompleteFailure(Throwable cause) { byteBufferIterator.close(); callback.failed(cause); } @@ -247,8 +236,7 @@ public void addCookie(ResponseCookie cookie) { cookies.add(cookie.getName(), cookie); } - private void initializeCookies() - { + private void initializeCookies() { cookies = new LinkedMultiValueMap<>(); for (HttpField f : response.getHeaders()) { if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) @@ -333,17 +321,20 @@ public Map getAttributes() { } } - private static class ContentWriterIteratingCallback extends IteratingCallback - { + private static class ContentWriterIteratingCallback extends IteratingCallback { private final SeekableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; - public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException - { + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException { this.source = content; this.sink = target; this.callback = callback; @@ -357,17 +348,15 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position } @Override - protected Action process() throws Throwable - { + protected Action process() throws Throwable { if (!source.isOpen() || totalRead == length) return Action.SUCCEEDED; ByteBuffer byteBuffer = buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); + byteBuffer.limit((int) Math.min(buffer.capacity(), length - totalRead)); int read = source.read(byteBuffer); - if (read == -1) - { + if (read == -1) { IO.close(source); sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; @@ -379,16 +368,14 @@ protected Action process() throws Throwable } @Override - protected void onCompleteSuccess() - { + protected void onCompleteSuccess() { buffer.release(); IO.close(source); callback.succeeded(); } @Override - protected void onCompleteFailure(Throwable x) - { + protected void onCompleteFailure(Throwable x) { buffer.release(); IO.close(source); callback.failed(x); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 5b9c2fbd1bd7..8c239c5cf7f8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -16,12 +16,6 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.io.Retainable; -import org.eclipse.jetty.server.Response; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; - import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -29,6 +23,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.IntPredicate; +import org.eclipse.jetty.io.Retainable; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; + /** * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} * @@ -38,7 +38,9 @@ */ public class JettyRetainedDataBuffer implements PooledDataBuffer { private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 40da34f914a8..dd756cc28c13 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,13 +16,10 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; -import org.eclipse.jetty.ee10.servlet.ServletHolder; -import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; + import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; -import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** * @author Rossen Stoyanchev From c8a7cc3cc4de042d335ff5ad94c042d3f77af5a6 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 15:49:03 +0900 Subject: [PATCH 023/146] Fixed unset response status bug Jetty has an unset response status of 0, this should be converted to 200 for spring. --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index e2cfc8e58b16..fc1cc8a898b9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -211,7 +211,8 @@ public boolean setStatusCode(@Nullable HttpStatusCode status) { @Override public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(response.getStatus()); + int status = response.getStatus(); + return HttpStatusCode.valueOf(status == 0 ? 200 : status); } @Override From 85bdb2b196af710d74a8e285c6b726298637715a Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 16:09:50 +0900 Subject: [PATCH 024/146] Request header mutation support When mutating the request, a mutable HttpFields instance is created. --- .../DefaultServerHttpRequestBuilder.java | 33 ++++++++++++++----- .../reactive/JettyCoreServerHttpRequest.java | 10 +++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 54e2b5892649..42e4b025f660 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -20,6 +20,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Objects; import java.util.function.Consumer; import reactor.core.publisher.Flux; @@ -67,14 +68,30 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { - Assert.notNull(original, "ServerHttpRequest is required"); - - this.uri = original.getURI(); - this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders()); - this.httpMethod = original.getMethod(); - this.contextPath = original.getPath().contextPath().value(); - this.remoteAddress = original.getRemoteAddress(); - this.body = original.getBody(); + this(original.getURI(), + HttpHeaders.writableHttpHeaders(original.getHeaders()), + original.getMethod(), + original.getPath().contextPath().value(), + original.getRemoteAddress(), + original.getBody(), + Objects.requireNonNull(original, "ServerHttpRequest is required")); + } + + + public DefaultServerHttpRequestBuilder( + URI uri, + HttpHeaders httpHeaders, + HttpMethod method, + String contextPath, + @Nullable InetSocketAddress remoteAddress, + Flux body, + ServerHttpRequest original) { + this.uri = uri; + this.headers = httpHeaders; + this.httpMethod = method; + this.contextPath = contextPath; + this.remoteAddress = remoteAddress; + this.body = body; this.originalRequest = original; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index db3787e631ae..46d3fdfed948 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,8 +22,10 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; @@ -189,6 +191,12 @@ public X509Certificate[] getPeerCertificates() { @Override public Builder mutate() { - return ServerHttpRequest.super.mutate(); + return new DefaultServerHttpRequestBuilder(this.getURI(), + new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(request.getHeaders()))), + this.getMethod(), + this.getPath().contextPath().value(), + this.getRemoteAddress(), + this.getBody(), + Objects.requireNonNull(this, "ServerHttpRequest is required")); } } From 39f9ce8cb071a55f078f7a81c88a33ae00650a85 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 16:22:00 +0900 Subject: [PATCH 025/146] Do not use canonical path Spring appears to want the unadulterated raw path complete with parameters etc. --- .../http/server/reactive/JettyCoreServerHttpRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 46d3fdfed948..31fbdad816a1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -80,7 +80,7 @@ public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request r this.dataBufferFactory = dataBufferFactory; this.request = request; headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); } @Override From c46382ea3d0d08d06bde834eb9bdc6bdb858bf1e Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 25 Jan 2024 09:45:02 +0900 Subject: [PATCH 026/146] Track leaks (for now) and retain DataBuffer chunks --- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 + .../server/reactive/bootstrap/JettyCoreHttpServer.java | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 8c239c5cf7f8..e6327997ccf3 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -46,6 +46,7 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { this.dataBuffer = dataBuffer; this.retainable = retainable; + this.retainable.retain(); } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index dd756cc28c13..71eb061ecbab 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,6 +16,7 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -29,12 +30,14 @@ */ public class JettyCoreHttpServer extends AbstractHttpServer { + private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove private Server jettyServer; @Override protected void initServer() { - this.jettyServer = new Server(); + this.byteBufferPool = new ArrayByteBufferPool.Tracking(); + this.jettyServer = new Server(null, null, byteBufferPool); ServerConnector connector = new ServerConnector(this.jettyServer); connector.setHost(getHost()); @@ -62,6 +65,11 @@ protected void stopInternal() { catch (Exception ex) { // ignore } + + // TODO remove this or make debug only + this.byteBufferPool.dumpLeaks(); + if (!this.byteBufferPool.getLeaks().isEmpty()) + throw new IllegalStateException("LEAKS"); } @Override From 72921c0791ed2ee4821c18645beced2a77575372 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 30 Jan 2024 18:29:30 +1100 Subject: [PATCH 027/146] Fixes for Jetty Core WebSocket Signed-off-by: Lachlan Roberts --- spring-web/spring-web.gradle | 1 + .../reactive/JettyCoreServerHttpRequest.java | 4 +- .../reactive/JettyCoreServerHttpResponse.java | 6 +- .../bootstrap/JettyCoreHttpServer.java | 7 +- spring-webflux/spring-webflux.gradle | 1 + .../adapter/JettyWebSocketHandlerAdapter.java | 12 +- .../JettyCoreRequestUpgradeStrategy.java | 167 ++++++++++++++++++ .../upgrade/JettyRequestUpgradeStrategy.java | 5 +- ...ractReactiveWebSocketIntegrationTests.java | 29 ++- 9 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index b1fffdb0c610..721c140400c9 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -70,6 +70,7 @@ dependencies { because("needed by Netty's SelfSignedCertificate on JDK 15+") } testFixturesImplementation("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") + testFixturesImplementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-context"))) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 31fbdad816a1..55f9b1defc11 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -31,8 +31,6 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -45,6 +43,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -54,6 +53,7 @@ * @author Greg Wilkins * @since 6.2 */ +// TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index fc1cc8a898b9..23c4aa36d7b0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,9 +40,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -54,6 +51,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} @@ -62,6 +61,7 @@ * @author Lachlan Roberts * @since 6.2 */ +// TODO: extend AbstractServerHttpResponse for websocket. class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 71eb061ecbab..c40dc6fa5538 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -19,7 +19,7 @@ import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; - +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; /** @@ -44,7 +44,10 @@ protected void initServer() { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(createHandlerAdapter()); - // TODO add websocket upgrade handler + + // TODO: We don't actually want the upgrade handler but this will create the WebSocketContainer. + // This requires a change in Jetty. + WebSocketUpgradeHandler.from(jettyServer); } private JettyCoreHttpHandlerAdapter createHandlerAdapter() { diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index e0f98bcdc8fa..396eba3dfe69 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -27,6 +27,7 @@ dependencies { optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 71c0a2c840c1..b8f38256495b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,7 +32,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.core.OpCode; - import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -53,18 +52,14 @@ */ @WebSocket public class JettyWebSocketHandlerAdapter { - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - private final WebSocketHandler delegateHandler; - private final Function sessionFactory; @Nullable private JettyWebSocketSession delegateSession; - public JettyWebSocketHandlerAdapter(WebSocketHandler handler, Function sessionFactory) { @@ -74,7 +69,6 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler handler, this.sessionFactory = sessionFactory; } - @OnWebSocketOpen public void onWebSocketOpen(Session session) { this.delegateSession = this.sessionFactory.apply(session); @@ -101,6 +95,9 @@ public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } + else { + callback.succeed(); + } } @OnWebSocketFrame @@ -112,8 +109,11 @@ public void onWebSocketFrame(Frame frame, Callback callback) { buffer = new JettyDataBuffer(buffer, callback); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + return; } } + + callback.succeed(); } @OnWebSocketClose diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java new file mode 100644 index 000000000000..94fdf5bda060 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.socket.server.upgrade; + +import java.lang.reflect.Field; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.websocket.api.Configurable; +import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; +import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; +import org.eclipse.jetty.websocket.server.WebSocketCreator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; +import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 Core. + * + * @author Rossen Stoyanchev + * @since 5.3.4 + */ +public class JettyCoreRequestUpgradeStrategy implements RequestUpgradeStrategy { + + @Nullable + private Consumer webSocketConfigurer; + + @Nullable + private ServerWebSocketContainer serverContainer; + + /** + * Add a callback to configure WebSocket server parameters on + * {@link JettyWebSocketServerContainer}. + * @since 6.1 + */ + public void addWebSocketConfigurer(Consumer webSocketConfigurer) { + this.webSocketConfigurer = (this.webSocketConfigurer != null ? + this.webSocketConfigurer.andThen(webSocketConfigurer) : webSocketConfigurer); + } + + private Request getJettyRequest(ServerHttpRequest request) + { + try + { + // TODO: JettyCoreServerHttpRequest should extend AbstractServerHttpRequest. + // This will allow the ServerHttpRequestDecorator.getNativeRequest(request) to extract the native request. + Field requestField = request.getClass().getDeclaredField("request"); + requestField.setAccessible(true); + return (Request)requestField.get(request); + } + catch (NoSuchFieldException | IllegalAccessException e) + { + return null; + } + } + + private Response getJettyResponse(ServerHttpResponse response) + { + try + { + // TODO: JettyCoreServerHttpResponse should extend AbstractServerHttpResponse. + // This will allow the ServerHttpRequestDecorator.getNativeResponse(response) to extract the native response. + Field requestField = response.getClass().getDeclaredField("response"); + requestField.setAccessible(true); + return (Response)requestField.get(response); + } + catch (NoSuchFieldException | IllegalAccessException e) + { + return null; + } + } + + @Override + public Mono upgrade( + ServerWebExchange exchange, WebSocketHandler handler, + @Nullable String subProtocol, Supplier handshakeInfoFactory) { + + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + + Request jettyRequest = getJettyRequest(request); + Response jettyResponse = getJettyResponse(response); + + HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); + DataBufferFactory factory = response.bufferFactory(); + + // Trigger WebFlux preCommit actions before upgrade + response.beforeCommit(() -> Mono.deferContextual(contextView -> + { + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new JettyWebSocketSession(session, handshakeInfo, factory)); + + WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> + { + if (subProtocol != null) + { + upgradeResponse.setAcceptedSubProtocol(subProtocol); + } + return adapter; + }; + + ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); + try + { + FutureCallback callback = new FutureCallback(); + if (container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) + { + callback.block(); + } + else + { + throw new WebSocketException("request could not be upgraded to websocket"); + } + } + catch (Exception ex) + { + return Mono.error(ex); + } + + return Mono.empty(); + })); + + return exchange.getResponse().setComplete(); + } + + private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { + if (this.serverContainer == null) { + Server server = jettyRequest.getConnectionMetaData().getConnector().getServer(); + ServerWebSocketContainer container = ServerWebSocketContainer.get(server.getContext()); + if (this.webSocketConfigurer != null) { + this.webSocketConfigurer.accept(container); + } + this.serverContainer = container; + } + return this.serverContainer; + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index 66b41aa3acf1..82711c47262c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -25,8 +25,6 @@ import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.websocket.api.Configurable; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -40,9 +38,10 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; /** - * A WebSocket {@code RequestUpgradeStrategy} for Jetty 11. + * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 EE10. * * @author Rossen Stoyanchev * @since 5.3.4 diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index fe5909ca8776..f991291c13f7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,13 +31,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; -import org.xnio.OptionMap; -import org.xnio.Xnio; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple3; - import org.springframework.context.ApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -54,6 +47,7 @@ import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.server.upgrade.JettyCoreRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.JettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNetty2RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; @@ -61,6 +55,17 @@ import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; +import org.xnio.OptionMap; +import org.xnio.Xnio; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple3; /** * Base class for reactive WebSocket integration tests. Subclasses must implement @@ -93,7 +98,7 @@ static Stream arguments() throws IOException { Map> servers = new LinkedHashMap<>(); servers.put(new TomcatHttpServer(TMP_DIR.getAbsolutePath(), WsContextListener.class), TomcatConfig.class); servers.put(new JettyHttpServer(), JettyConfig.class); - servers.put(new JettyCoreHttpServer(), JettyConfig.class); + servers.put(new JettyCoreHttpServer(), JettyCoreConfig.class); servers.put(new ReactorHttpServer(), ReactorNettyConfig.class); servers.put(new UndertowHttpServer(), UndertowConfig.class); @@ -238,4 +243,12 @@ protected RequestUpgradeStrategy getUpgradeStrategy() { } } + @Configuration + static class JettyCoreConfig extends AbstractHandlerAdapterConfig { + + @Override + protected RequestUpgradeStrategy getUpgradeStrategy() { + return new JettyCoreRequestUpgradeStrategy(); + } + } } From 05df5d1dcf69e9bfa976c1274026d16d9b4d9e1a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 30 Jan 2024 19:33:16 +1100 Subject: [PATCH 028/146] Temporary fix for cookies in JettyCoreServerHttpResponse with TODOs Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 23c4aa36d7b0..0bb4db0e311d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -51,6 +51,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.MultiValueMapAdapter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -147,19 +148,22 @@ public Mono writeWith(Path file, long position, long count) { private Mono ensureCommitted() { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { + // TODO: WebSocket upgrade bypasses this response and writes directly with the Jetty Response. + // Because of this our attempt at doing writeCookies doesn't work because some commitActions + // are settings cookies on this instance, but websocket needs them set before because it will do the commit. return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) + //.concatWith(Mono.fromRunnable(this::writeCookies)) .then() .doOnError(t -> getHeaders().clearContentHeaders()); } - doCommit(); + // writeCookies(); } return null; } - private void doCommit() { + private void writeCookies() { if (cookies != null) { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() @@ -227,14 +231,35 @@ public boolean setRawStatusCode(@Nullable Integer value) { public MultiValueMap getCookies() { if (cookies == null) initializeCookies(); - return cookies; + + // TODO: not good enough. + return new MultiValueMapAdapter<>(cookies) + { + @Override + public void add(String key, ResponseCookie value) + { + Response.addCookie(response, new HttpResponseCookie(value)); + super.add(key, value); + } + + @Override + public void set(String key, ResponseCookie value) + { + Response.putCookie(response, new HttpResponseCookie(value)); + super.set(key, value); + } + }; } @Override public void addCookie(ResponseCookie cookie) { + /* if (cookies == null) initializeCookies(); cookies.add(cookie.getName(), cookie); + */ + + Response.addCookie(response, new HttpResponseCookie(cookie)); } private void initializeCookies() { From 89dbcbfcc76dfa7e8bc9881facf49da555de4aac Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 00:38:33 +1100 Subject: [PATCH 029/146] Do not actually commit response in JettyCoreServerHttpResponse & fix cookies Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 42 +++---------------- .../JettyCoreRequestUpgradeStrategy.java | 27 ++++-------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 0bb4db0e311d..2d8ccd093b30 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -51,7 +51,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.MultiValueMapAdapter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -97,7 +96,7 @@ public void beforeCommit(Supplier> action) { @Override public boolean isCommitted() { - return response.isCommitted(); + return committed.get(); } @Override @@ -117,13 +116,8 @@ public Mono writeAndFlushWith(Publisher setComplete() { Mono mono = ensureCommitted(); - if (mono != null) - return mono.then(Mono.defer(this::setComplete)); - - Callback.Completable callback = new Callback.Completable(); - response.write(true, BufferUtil.EMPTY_BUFFER, callback); - return Mono.fromFuture(callback); - } + return (mono == null) ? Mono.empty() : mono; + } @Override public Mono writeWith(Path file, long position, long count) { @@ -148,16 +142,13 @@ public Mono writeWith(Path file, long position, long count) { private Mono ensureCommitted() { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { - // TODO: WebSocket upgrade bypasses this response and writes directly with the Jetty Response. - // Because of this our attempt at doing writeCookies doesn't work because some commitActions - // are settings cookies on this instance, but websocket needs them set before because it will do the commit. return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - //.concatWith(Mono.fromRunnable(this::writeCookies)) + .concatWith(Mono.fromRunnable(this::writeCookies)) .then() .doOnError(t -> getHeaders().clearContentHeaders()); } - // writeCookies(); + writeCookies(); } return null; @@ -231,35 +222,14 @@ public boolean setRawStatusCode(@Nullable Integer value) { public MultiValueMap getCookies() { if (cookies == null) initializeCookies(); - - // TODO: not good enough. - return new MultiValueMapAdapter<>(cookies) - { - @Override - public void add(String key, ResponseCookie value) - { - Response.addCookie(response, new HttpResponseCookie(value)); - super.add(key, value); - } - - @Override - public void set(String key, ResponseCookie value) - { - Response.putCookie(response, new HttpResponseCookie(value)); - super.set(key, value); - } - }; + return cookies; } @Override public void addCookie(ResponseCookie cookie) { - /* if (cookies == null) initializeCookies(); cookies.add(cookie.getName(), cookie); - */ - - Response.addCookie(response, new HttpResponseCookie(cookie)); } private void initializeCookies() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 94fdf5bda060..72f83a1c04a8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -24,7 +24,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.websocket.api.Configurable; import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; @@ -113,8 +113,8 @@ public Mono upgrade( DataBufferFactory factory = response.bufferFactory(); // Trigger WebFlux preCommit actions before upgrade - response.beforeCommit(() -> Mono.deferContextual(contextView -> - { + return exchange.getResponse().setComplete() + .then(Mono.deferContextual(contextView -> { JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( ContextWebSocketHandler.decorate(handler, contextView), session -> new JettyWebSocketSession(session, handshakeInfo, factory)); @@ -122,34 +122,25 @@ public Mono upgrade( WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> { if (subProtocol != null) - { upgradeResponse.setAcceptedSubProtocol(subProtocol); - } return adapter; }; + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); try { - FutureCallback callback = new FutureCallback(); - if (container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) - { - callback.block(); - } - else - { + if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) throw new WebSocketException("request could not be upgraded to websocket"); - } } - catch (Exception ex) + catch (WebSocketException e) { - return Mono.error(ex); + callback.failed(e); } - return Mono.empty(); + return mono; })); - - return exchange.getResponse().setComplete(); } private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { From 654407ede367c1915ace8a1358a76658213dad7c Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 00:40:48 +1100 Subject: [PATCH 030/146] Add JettyCore upgrade option in HandshakeWebSocketService Signed-off-by: Lachlan Roberts --- .../server/support/HandshakeWebSocketService.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java index 7392a8c18859..41832cc62a7a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java @@ -27,8 +27,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; - import org.springframework.context.Lifecycle; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.WebSocketService; +import org.springframework.web.reactive.socket.server.upgrade.JettyCoreRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.JettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNetty2RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; @@ -52,6 +51,7 @@ import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; /** * {@code WebSocketService} implementation that handles a WebSocket HTTP @@ -76,6 +76,8 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle { private static final boolean jettyWsPresent; + private static final boolean jettyCoreWsPresent; + private static final boolean undertowWsPresent; private static final boolean reactorNettyPresent; @@ -88,6 +90,8 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle { "org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", classLoader); jettyWsPresent = ClassUtils.isPresent( "org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer", classLoader); + jettyCoreWsPresent = ClassUtils.isPresent( + "org.eclipse.jetty.websocket.server.ServerWebSocketContainer", classLoader); undertowWsPresent = ClassUtils.isPresent( "io.undertow.websockets.WebSocketProtocolHandshakeHandler", classLoader); reactorNettyPresent = ClassUtils.isPresent( @@ -277,6 +281,9 @@ static RequestUpgradeStrategy initUpgradeStrategy() { else if (jettyWsPresent) { return new JettyRequestUpgradeStrategy(); } + else if (jettyCoreWsPresent) { + return new JettyCoreRequestUpgradeStrategy(); + } else if (undertowWsPresent) { return new UndertowRequestUpgradeStrategy(); } From 802419e98bbb663729800e4d43d9b3d5dcde9d58 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 01:00:05 +1100 Subject: [PATCH 031/146] move getNativeRequest/Response to ServerHttpRequest/Response interfaces Signed-off-by: Lachlan Roberts --- .../reactive/AbstractServerHttpRequest.java | 7 ---- .../reactive/AbstractServerHttpResponse.java | 13 +------ .../reactive/JettyCoreServerHttpRequest.java | 7 ++++ .../reactive/JettyCoreServerHttpResponse.java | 7 ++++ .../server/reactive/ServerHttpRequest.java | 11 ++++++ .../reactive/ServerHttpRequestDecorator.java | 20 +++++----- .../server/reactive/ServerHttpResponse.java | 10 +++++ .../reactive/ServerHttpResponseDecorator.java | 19 ++++----- .../server/DefaultServerRequestBuilder.java | 11 ++++-- .../JettyCoreRequestUpgradeStrategy.java | 39 ++----------------- .../result/view/ZeroDemandResponse.java | 10 +++-- 11 files changed, 77 insertions(+), 77 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index da4659cd3e61..9e4f1547e171 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -203,13 +203,6 @@ public SslInfo getSslInfo() { @Nullable protected abstract SslInfo initSslInfo(); - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - public abstract T getNativeRequest(); - /** * For internal use in logging at the HTTP adapter layer. * @since 5.1 diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 7a06126ce85d..8885a2649efc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -23,9 +23,6 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -37,6 +34,8 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Base class for {@link ServerHttpResponse} implementations. @@ -155,14 +154,6 @@ public void addCookie(ResponseCookie cookie) { } } - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - public abstract T getNativeResponse(); - - @Override public void beforeCommit(Supplier> action) { this.commitActions.add(action); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 55f9b1defc11..59d42cc05f42 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -189,6 +189,13 @@ public X509Certificate[] getPeerCertificates() { return null; } + @SuppressWarnings("unchecked") + @Override + public T getNativeRequest() + { + return (T) request; + } + @Override public Builder mutate() { return new DefaultServerHttpRequestBuilder(this.getURI(), diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 2d8ccd093b30..3196392679fb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -232,6 +232,13 @@ public void addCookie(ResponseCookie cookie) { cookies.add(cookie.getName(), cookie); } + @SuppressWarnings("unchecked") + @Override + public T getNativeResponse() + { + return (T) response; + } + private void initializeCookies() { cookies = new LinkedMultiValueMap<>(); for (HttpField f : response.getHeaders()) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 55f5fa2c654b..5baa3ac51912 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -97,6 +97,17 @@ default SslInfo getSslInfo() { return null; } + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + @Nullable + default T getNativeRequest() + { + return null; + } + /** * Return a builder to mutate properties of this request by wrapping it * with {@link ServerHttpRequestDecorator} and returning either mutated diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index fc6143bfdaf0..05e92ab8395a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -19,8 +19,6 @@ import java.net.InetSocketAddress; import java.net.URI; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -29,6 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; /** * Wraps another {@link ServerHttpRequest} and delegates all methods to it. @@ -108,6 +107,12 @@ public SslInfo getSslInfo() { return getDelegate().getSslInfo(); } + @Override + public T getNativeRequest() + { + return delegate.getNativeRequest(); + } + @Override public Flux getBody() { return getDelegate().getBody(); @@ -123,16 +128,13 @@ public Flux getBody() { * @since 5.3.3 */ public static T getNativeRequest(ServerHttpRequest request) { - if (request instanceof AbstractServerHttpRequest abstractServerHttpRequest) { - return abstractServerHttpRequest.getNativeRequest(); - } - else if (request instanceof ServerHttpRequestDecorator serverHttpRequestDecorator) { - return getNativeRequest(serverHttpRequestDecorator.getDelegate()); - } - else { + T nativeRequest = request.getNativeRequest(); + if (nativeRequest == null) { throw new IllegalArgumentException( "Can't find native request in " + request.getClass().getName()); } + + return nativeRequest; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 7f97d04484a5..eb61473448e1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -86,4 +86,14 @@ default Integer getRawStatusCode() { */ void addCookie(ResponseCookie cookie); + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + @Nullable + default T getNativeResponse() + { + return null; + } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index e336284d38fd..dc27cb2984bf 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -19,8 +19,6 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; @@ -29,6 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; /** * Wraps another {@link ServerHttpResponse} and delegates all methods to it. @@ -121,6 +120,11 @@ public Mono setComplete() { return getDelegate().setComplete(); } + @Override + public T getNativeResponse() + { + return getDelegate().getNativeResponse(); + } /** * Return the native response of the underlying server API, if possible, @@ -131,16 +135,13 @@ public Mono setComplete() { * @since 5.3.3 */ public static T getNativeResponse(ServerHttpResponse response) { - if (response instanceof AbstractServerHttpResponse abstractServerHttpResponse) { - return abstractServerHttpResponse.getNativeResponse(); - } - else if (response instanceof ServerHttpResponseDecorator serverHttpResponseDecorator) { - return getNativeResponse(serverHttpResponseDecorator.getDelegate()); - } - else { + T nativeResponse = response.getNativeResponse(); + if (nativeResponse == null) { throw new IllegalArgumentException( "Can't find native response in " + response.getClass().getName()); } + + return nativeResponse; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index a97fbd902563..bb22457df381 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -28,9 +28,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.context.ApplicationContext; import org.springframework.context.i18n.LocaleContext; import org.springframework.core.ResolvableType; @@ -57,6 +54,8 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Default {@link ServerRequest.Builder} implementation. @@ -297,6 +296,12 @@ public MultiValueMap getQueryParams() { public Flux getBody() { return this.body; } + + @Override + public T getNativeRequest() + { + return null; + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 72f83a1c04a8..66cab235901b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.socket.server.upgrade; -import java.lang.reflect.Field; import java.util.function.Consumer; import java.util.function.Supplier; @@ -31,7 +30,9 @@ import org.eclipse.jetty.websocket.server.WebSocketCreator; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.lang.Nullable; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -66,38 +67,6 @@ public void addWebSocketConfigurer(Consumer webSocketConfigurer) { this.webSocketConfigurer.andThen(webSocketConfigurer) : webSocketConfigurer); } - private Request getJettyRequest(ServerHttpRequest request) - { - try - { - // TODO: JettyCoreServerHttpRequest should extend AbstractServerHttpRequest. - // This will allow the ServerHttpRequestDecorator.getNativeRequest(request) to extract the native request. - Field requestField = request.getClass().getDeclaredField("request"); - requestField.setAccessible(true); - return (Request)requestField.get(request); - } - catch (NoSuchFieldException | IllegalAccessException e) - { - return null; - } - } - - private Response getJettyResponse(ServerHttpResponse response) - { - try - { - // TODO: JettyCoreServerHttpResponse should extend AbstractServerHttpResponse. - // This will allow the ServerHttpRequestDecorator.getNativeResponse(response) to extract the native response. - Field requestField = response.getClass().getDeclaredField("response"); - requestField.setAccessible(true); - return (Response)requestField.get(response); - } - catch (NoSuchFieldException | IllegalAccessException e) - { - return null; - } - } - @Override public Mono upgrade( ServerWebExchange exchange, WebSocketHandler handler, @@ -106,8 +75,8 @@ public Mono upgrade( ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); - Request jettyRequest = getJettyRequest(request); - Response jettyResponse = getJettyResponse(response); + Request jettyRequest = ServerHttpRequestDecorator.getNativeRequest(request); + Response jettyResponse = ServerHttpResponseDecorator.getNativeResponse(response); HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); DataBufferFactory factory = response.bufferFactory(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index b543447e1e68..afa859b6cbf3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -20,9 +20,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; @@ -31,6 +28,8 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Mono; /** * Response that subscribes to the writes source but never posts demand and also @@ -116,6 +115,11 @@ public HttpHeaders getHeaders() { throw new UnsupportedOperationException(); } + @Override + public T getNativeResponse() + { + return null; + } private static class ZeroDemandSubscriber extends BaseSubscriber { From 45181382f11acda9d2622e5001d362ecfd88fb73 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 18:36:52 +0900 Subject: [PATCH 032/146] Reformatted code --- .../reactive/AbstractServerHttpResponse.java | 5 +- .../DefaultServerHttpRequestBuilder.java | 7 +- .../reactive/JettyCoreHttpHandlerAdapter.java | 6 +- .../reactive/JettyCoreServerHttpRequest.java | 91 +++++++------ .../reactive/JettyCoreServerHttpResponse.java | 128 ++++++++++-------- .../reactive/JettyRetainedDataBuffer.java | 88 ++++++------ .../server/reactive/ServerHttpRequest.java | 3 +- .../reactive/ServerHttpRequestDecorator.java | 8 +- .../server/reactive/ServerHttpResponse.java | 3 +- .../reactive/ServerHttpResponseDecorator.java | 6 +- .../http/support/JettyHeadersAdapter.java | 23 +++- .../ErrorHandlerIntegrationTests.java | 7 +- .../reactive/ZeroCopyIntegrationTests.java | 8 +- .../bootstrap/JettyCoreHttpServer.java | 2 + .../server/DefaultServerRequestBuilder.java | 8 +- .../adapter/JettyWebSocketHandlerAdapter.java | 2 + .../support/HandshakeWebSocketService.java | 3 +- .../JettyCoreRequestUpgradeStrategy.java | 54 ++++---- .../upgrade/JettyRequestUpgradeStrategy.java | 3 +- .../ContextPathIntegrationTests.java | 14 +- .../annotation/SseIntegrationTests.java | 51 ++++--- .../result/view/ZeroDemandResponse.java | 8 +- ...ractReactiveWebSocketIntegrationTests.java | 11 +- 23 files changed, 294 insertions(+), 245 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 8885a2649efc..82731c218d90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -23,6 +23,9 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -34,8 +37,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Base class for {@link ServerHttpResponse} implementations. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 42e4b025f660..9fce1464440e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -70,10 +70,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { this(original.getURI(), HttpHeaders.writableHttpHeaders(original.getHeaders()), - original.getMethod(), - original.getPath().contextPath().value(), - original.getRemoteAddress(), - original.getBody(), + original.getMethod(),original.getPath().contextPath().value(), + original.getRemoteAddress(), + original.getBody(), Objects.requireNonNull(original, "ServerHttpRequest is required")); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 48841a7b0a00..a687d03ea791 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.util.Callback; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -35,6 +36,7 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; + private final DataBufferFactory dataBufferFactory; public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @@ -44,12 +46,12 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { // because we mainly use wrap and there should be few allocation done by the factory. // But it could be possible to use the servers buffer pool for allocations and to // create PooledDataBuffers - dataBufferFactory = new DefaultDataBufferFactory(); + this.dataBufferFactory = new DefaultDataBufferFactory(); } @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) + this.httpHandler.handle(new JettyCoreServerHttpRequest(this.dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 59d42cc05f42..02c911e1a58a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; -import java.util.Objects; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -31,6 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -43,21 +44,20 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** - * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} + * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. * * @author Greg Wilkins * @since 6.2 */ // TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { - private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private static final MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private final DataBufferFactory dataBufferFactory; @@ -79,25 +79,26 @@ class JettyCoreServerHttpRequest implements ServerHttpRequest { public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { this.dataBufferFactory = dataBufferFactory; this.request = request; - headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); + this.headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + this.path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); } @Override public HttpHeaders getHeaders() { - return headers; + return this.headers; } @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(request.getMethod()); + return HttpMethod.valueOf(this.request.getMethod()); } @Override public URI getURI() { - if (uri == null) - uri = request.getHttpURI().toURI(); - return uri; + if (this.uri == null) { + this.uri = this.request.getHttpURI().toURI(); + } + return this.uri; } @Override @@ -105,75 +106,78 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap); } private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + return new JettyRetainedDataBuffer(this.dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); } @Override public String getId() { - return request.getId(); + return this.request.getId(); } @Override public RequestPath getPath() { - return path; + return this.path; } @Override public MultiValueMap getQueryParams() { - if (queryParameters == null) { - String query = request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) - queryParameters = EMPTY_QUERY; + if (this.queryParameters == null) { + String query = this.request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) { + this.queryParameters = EMPTY_QUERY; + } else { - queryParameters = new LinkedMultiValueMap<>(); + this.queryParameters = new LinkedMultiValueMap<>(); Matcher matcher = QUERY_PATTERN.matcher(query); while (matcher.find()) { String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); String eq = matcher.group(2); String value = matcher.group(3); value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - queryParameters.add(name, value); + this.queryParameters.add(name, value); } } } - return queryParameters; + return this.queryParameters; } @Override public MultiValueMap getCookies() { - if (cookies == null) { - List httpCookies = Request.getCookies(request); - if (httpCookies.isEmpty()) - cookies = EMPTY_COOKIES; + if (this.cookies == null) { + List httpCookies = Request.getCookies(this.request); + if (httpCookies.isEmpty()) { + this.cookies = EMPTY_COOKIES; + } else { - cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) - cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); + this.cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + this.cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + this.cookies = CollectionUtils.unmodifiableMultiValueMap(this.cookies); } } - return cookies; + return this.cookies; } @Override public InetSocketAddress getLocalAddress() { - return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override public InetSocketAddress getRemoteAddress() { - return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override public SslInfo getSslInfo() { - if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + if (this.request.getConnectionMetaData().isSecure() && this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { return new SslInfo() { @Override public String getSessionId() { @@ -191,19 +195,18 @@ public X509Certificate[] getPeerCertificates() { @SuppressWarnings("unchecked") @Override - public T getNativeRequest() - { - return (T) request; + public T getNativeRequest() { + return (T) this.request; } @Override public Builder mutate() { return new DefaultServerHttpRequestBuilder(this.getURI(), - new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(request.getHeaders()))), - this.getMethod(), - this.getPath().contextPath().value(), - this.getRemoteAddress(), - this.getBody(), - Objects.requireNonNull(this, "ServerHttpRequest is required")); + new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(this.request.getHeaders()))), + this.getMethod(), + this.getPath().contextPath().value(), + this.getRemoteAddress(), + this.getBody(), + this); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3196392679fb..09261ef76c52 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,6 +40,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -51,11 +54,9 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** - * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} + * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. * * @author Greg Wilkins * @author Lachlan Roberts @@ -76,12 +77,12 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOut public JettyCoreServerHttpResponse(Response response) { this.response = response; - headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); } @Override public HttpHeaders getHeaders() { - return headers; + return this.headers; } @Override @@ -91,12 +92,12 @@ public DataBufferFactory bufferFactory() { @Override public void beforeCommit(Supplier> action) { - commitActions.add(action); + this.commitActions.add(action); } @Override public boolean isCommitted() { - return committed.get(); + return this.committed.get(); } @Override @@ -116,31 +117,33 @@ public Mono writeAndFlushWith(Publisher setComplete() { Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; - } + return (mono == null) ? Mono.empty() : mono; + } @Override public Mono writeWith(Path file, long position, long count) { Mono mono = ensureCommitted(); - if (mono != null) + if (mono != null) { return mono.then(Mono.defer(() -> writeWith(file, position, count))); + } Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { // TODO: Why does intellij warn about possible blocking call? + // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); - new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); + new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } - catch (Throwable t) { - callback.failed(t); + catch (Throwable th) { + callback.failed(th); } return mono; } @Nullable private Mono ensureCommitted() { - if (committed.compareAndSet(false, true)) { + if (this.committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -148,25 +151,26 @@ private Mono ensureCommitted() { .doOnError(t -> getHeaders().clearContentHeaders()); } - writeCookies(); + writeCookies(); } return null; } private void writeCookies() { - if (cookies != null) { + if (this.cookies != null) { // TODO: are we doubling up on cookies already existing in response? - cookies.values().stream() + this.cookies.values().stream() .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); + .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); } } private Mono sendDataBuffer(DataBuffer dataBuffer) { Mono mono = ensureCommitted(); - if (mono != null) + if (mono != null) { return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + } @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); @@ -174,8 +178,9 @@ private Mono sendDataBuffer(DataBuffer dataBuffer) { new IteratingCallback() { @Override protected Action process() { - if (!byteBufferIterator.hasNext()) + if (!byteBufferIterator.hasNext()) { return Action.SUCCEEDED; + } response.write(false, byteBufferIterator.next(), this); return Action.SCHEDULED; } @@ -198,52 +203,56 @@ protected void onCompleteFailure(Throwable cause) { @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) + if (isCommitted() || status == null) { return false; - response.setStatus(status.value()); + } + this.response.setStatus(status.value()); return true; } @Override public HttpStatusCode getStatusCode() { - int status = response.getStatus(); + int status = this.response.getStatus(); return HttpStatusCode.valueOf(status == 0 ? 200 : status); } @Override public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) + if (isCommitted() || value == null) { return false; - response.setStatus(value); + } + this.response.setStatus(value); return true; } @Override public MultiValueMap getCookies() { - if (cookies == null) + if (this.cookies == null) { initializeCookies(); - return cookies; + } + return this.cookies; } @Override public void addCookie(ResponseCookie cookie) { - if (cookies == null) + if (this.cookies == null) { initializeCookies(); - cookies.add(cookie.getName(), cookie); + } + this.cookies.add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @Override - public T getNativeResponse() - { - return (T) response; + public T getNativeResponse() { + return (T) this.response; } private void initializeCookies() { - cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + this.cookies = new LinkedMultiValueMap<>(); + for (HttpField f : this.response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) { + this.cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } } } @@ -255,17 +264,17 @@ public HttpResponseCookie(ResponseCookie responseCookie) { } public ResponseCookie getResponseCookie() { - return responseCookie; + return this.responseCookie; } @Override public String getName() { - return responseCookie.getName(); + return this.responseCookie.getName(); } @Override public String getValue() { - return responseCookie.getValue(); + return this.responseCookie.getValue(); } @Override @@ -275,7 +284,7 @@ public int getVersion() { @Override public long getMaxAge() { - return responseCookie.getMaxAge().toSeconds(); + return this.responseCookie.getMaxAge().toSeconds(); } @Override @@ -287,18 +296,18 @@ public String getComment() { @Override @Nullable public String getDomain() { - return responseCookie.getDomain(); + return this.responseCookie.getDomain(); } @Override @Nullable public String getPath() { - return responseCookie.getPath(); + return this.responseCookie.getPath(); } @Override public boolean isSecure() { - return responseCookie.isSecure(); + return this.responseCookie.isSecure(); } @Nullable @@ -310,7 +319,7 @@ public SameSite getSameSite() { @Override public boolean isHttpOnly() { - return responseCookie.isHttpOnly(); + return this.responseCookie.isHttpOnly(); } @Override @@ -342,7 +351,7 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position this.sink = target; this.callback = callback; this.length = count; - source.position(position); + this.source.position(position); ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); @@ -352,36 +361,37 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position @Override protected Action process() throws Throwable { - if (!source.isOpen() || totalRead == length) + if (!this.source.isOpen() || this.totalRead == this.length) { return Action.SUCCEEDED; + } - ByteBuffer byteBuffer = buffer.getByteBuffer(); + ByteBuffer byteBuffer = this.buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int) Math.min(buffer.capacity(), length - totalRead)); - int read = source.read(byteBuffer); + byteBuffer.limit((int) Math.min(this.buffer.capacity(), this.length - this.totalRead)); + int read = this.source.read(byteBuffer); if (read == -1) { - IO.close(source); - sink.write(true, BufferUtil.EMPTY_BUFFER, this); + IO.close(this.source); + this.sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; } - totalRead += read; + this.totalRead += read; BufferUtil.flipToFlush(byteBuffer, 0); - sink.write(false, byteBuffer, this); + this.sink.write(false, byteBuffer, this); return Action.SCHEDULED; } @Override protected void onCompleteSuccess() { - buffer.release(); - IO.close(source); - callback.succeeded(); + this.buffer.release(); + IO.close(this.source); + this.callback.succeeded(); } @Override protected void onCompleteFailure(Throwable x) { - buffer.release(); - IO.close(source); - callback.failed(x); + this.buffer.release(); + IO.close(this.source); + this.callback.failed(x); } } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index e6327997ccf3..77af0e288986 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -30,7 +30,7 @@ import org.springframework.core.io.buffer.PooledDataBuffer; /** - * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} + * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer}. * * @author Greg Wilkins * @author Lachlan Roberts @@ -51,12 +51,12 @@ public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { @Override public boolean isAllocated() { - return allocated.get(); + return this.allocated.get(); } @Override public PooledDataBuffer retain() { - retainable.retain(); + this.retainable.retain(); return this; } @@ -67,8 +67,8 @@ public PooledDataBuffer touch(Object hint) { @Override public boolean release() { - if (retainable.release()) { - allocated.set(false); + if (this.retainable.release()) { + this.allocated.set(false); return true; } return false; @@ -76,204 +76,204 @@ public boolean release() { @Override public DataBufferFactory factory() { - return dataBuffer.factory(); + return this.dataBuffer.factory(); } @Override public int indexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.indexOf(predicate, fromIndex); + return this.dataBuffer.indexOf(predicate, fromIndex); } @Override public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.lastIndexOf(predicate, fromIndex); + return this.dataBuffer.lastIndexOf(predicate, fromIndex); } @Override public int readableByteCount() { - return dataBuffer.readableByteCount(); + return this.dataBuffer.readableByteCount(); } @Override public int writableByteCount() { - return dataBuffer.writableByteCount(); + return this.dataBuffer.writableByteCount(); } @Override public int capacity() { - return dataBuffer.capacity(); + return this.dataBuffer.capacity(); } @Override @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { - return dataBuffer.capacity(capacity); + return this.dataBuffer.capacity(capacity); } @Override @Deprecated(since = "6.0") public DataBuffer ensureCapacity(int capacity) { - return dataBuffer.ensureCapacity(capacity); + return this.dataBuffer.ensureCapacity(capacity); } @Override public DataBuffer ensureWritable(int capacity) { - return dataBuffer.ensureWritable(capacity); + return this.dataBuffer.ensureWritable(capacity); } @Override public int readPosition() { - return dataBuffer.readPosition(); + return this.dataBuffer.readPosition(); } @Override public DataBuffer readPosition(int readPosition) { - return dataBuffer.readPosition(readPosition); + return this.dataBuffer.readPosition(readPosition); } @Override public int writePosition() { - return dataBuffer.writePosition(); + return this.dataBuffer.writePosition(); } @Override public DataBuffer writePosition(int writePosition) { - return dataBuffer.writePosition(writePosition); + return this.dataBuffer.writePosition(writePosition); } @Override public byte getByte(int index) { - return dataBuffer.getByte(index); + return this.dataBuffer.getByte(index); } @Override public byte read() { - return dataBuffer.read(); + return this.dataBuffer.read(); } @Override public DataBuffer read(byte[] destination) { - return dataBuffer.read(destination); + return this.dataBuffer.read(destination); } @Override public DataBuffer read(byte[] destination, int offset, int length) { - return dataBuffer.read(destination, offset, length); + return this.dataBuffer.read(destination, offset, length); } @Override public DataBuffer write(byte b) { - return dataBuffer.write(b); + return this.dataBuffer.write(b); } @Override public DataBuffer write(byte[] source) { - return dataBuffer.write(source); + return this.dataBuffer.write(source); } @Override public DataBuffer write(byte[] source, int offset, int length) { - return dataBuffer.write(source, offset, length); + return this.dataBuffer.write(source, offset, length); } @Override public DataBuffer write(DataBuffer... buffers) { - return dataBuffer.write(buffers); + return this.dataBuffer.write(buffers); } @Override public DataBuffer write(ByteBuffer... buffers) { - return dataBuffer.write(buffers); + return this.dataBuffer.write(buffers); } @Override public DataBuffer write(CharSequence charSequence, Charset charset) { - return dataBuffer.write(charSequence, charset); + return this.dataBuffer.write(charSequence, charset); } @Override @Deprecated(since = "6.0") public DataBuffer slice(int index, int length) { - return dataBuffer.slice(index, length); + return this.dataBuffer.slice(index, length); } @Override @Deprecated(since = "6.0") public DataBuffer retainedSlice(int index, int length) { - return dataBuffer.retainedSlice(index, length); + return this.dataBuffer.retainedSlice(index, length); } @Override public DataBuffer split(int index) { - return dataBuffer.split(index); + return this.dataBuffer.split(index); } @Override @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { - return dataBuffer.asByteBuffer(); + return this.dataBuffer.asByteBuffer(); } @Override @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { - return dataBuffer.asByteBuffer(index, length); + return this.dataBuffer.asByteBuffer(index, length); } @Override @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer() { - return dataBuffer.toByteBuffer(); + return this.dataBuffer.toByteBuffer(); } @Override @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { - return dataBuffer.toByteBuffer(index, length); + return this.dataBuffer.toByteBuffer(index, length); } @Override public void toByteBuffer(ByteBuffer dest) { - dataBuffer.toByteBuffer(dest); + this.dataBuffer.toByteBuffer(dest); } @Override public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + this.dataBuffer.toByteBuffer(srcPos, dest, destPos, length); } @Override public ByteBufferIterator readableByteBuffers() { - return dataBuffer.readableByteBuffers(); + return this.dataBuffer.readableByteBuffers(); } @Override public ByteBufferIterator writableByteBuffers() { - return dataBuffer.writableByteBuffers(); + return this.dataBuffer.writableByteBuffers(); } @Override public InputStream asInputStream() { - return dataBuffer.asInputStream(); + return this.dataBuffer.asInputStream(); } @Override public InputStream asInputStream(boolean releaseOnClose) { - return dataBuffer.asInputStream(releaseOnClose); + return this.dataBuffer.asInputStream(releaseOnClose); } @Override public OutputStream asOutputStream() { - return dataBuffer.asOutputStream(); + return this.dataBuffer.asOutputStream(); } @Override public String toString(Charset charset) { - return dataBuffer.toString(charset); + return this.dataBuffer.toString(charset); } @Override public String toString(int index, int length, Charset charset) { - return dataBuffer.toString(index, length, charset); + return this.dataBuffer.toString(index, length, charset); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 5baa3ac51912..41814c35aeb0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -103,8 +103,7 @@ default SslInfo getSslInfo() { * use such as WebSocket upgrades in the spring-webflux module. */ @Nullable - default T getNativeRequest() - { + default T getNativeRequest() { return null; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 05e92ab8395a..559ff010efa6 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -19,6 +19,8 @@ import java.net.InetSocketAddress; import java.net.URI; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -27,7 +29,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; /** * Wraps another {@link ServerHttpRequest} and delegates all methods to it. @@ -108,9 +109,8 @@ public SslInfo getSslInfo() { } @Override - public T getNativeRequest() - { - return delegate.getNativeRequest(); + public T getNativeRequest() { + return this.delegate.getNativeRequest(); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index eb61473448e1..5ab1708957a7 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -92,8 +92,7 @@ default Integer getRawStatusCode() { * use such as WebSocket upgrades in the spring-webflux module. */ @Nullable - default T getNativeResponse() - { + default T getNativeResponse() { return null; } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index dc27cb2984bf..4bc91e90d285 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -19,6 +19,8 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; @@ -27,7 +29,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Mono; /** * Wraps another {@link ServerHttpResponse} and delegates all methods to it. @@ -121,8 +122,7 @@ public Mono setComplete() { } @Override - public T getNativeResponse() - { + public T getNativeResponse() { return getDelegate().getNativeResponse(); } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index 703db7254802..e57bd76e8b2c 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -16,7 +16,14 @@ package org.springframework.http.support; -import java.util.*; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -38,6 +45,7 @@ public final class JettyHeadersAdapter implements MultiValueMap { private final HttpFields headers; + @Nullable private final HttpFields.Mutable mutable; @@ -163,7 +171,8 @@ public List put(String key, List value) { case 1 -> { if (oldValues == null) { mutableHttpFields.add(key, value.get(0)); - } else { + } + else { mutableHttpFields.put(key, value.get(0)); } } @@ -178,9 +187,8 @@ public List remove(Object key) { HttpFields.Mutable mutableHttpFields = mutableFields(); List list = null; if (key instanceof String name) { - for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext();) - { - HttpField f = i.next(); + for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext(); ) { + HttpField f = i.next(); if (f.is(name)) { if (list == null) { list = new ArrayList<>(); @@ -222,6 +230,7 @@ public Set>> entrySet() { public Iterator>> iterator() { return new EntryIterator(); } + @Override public int size() { return headers.size(); @@ -230,10 +239,10 @@ public int size() { } private HttpFields.Mutable mutableFields() { - if (mutable == null) { + if (this.mutable == null) { throw new IllegalStateException("Immutable headers"); } - return mutable; + return this.mutable; } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java index 3b1ec06db41a..e0d870bef2c3 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -18,7 +18,6 @@ import java.net.URI; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; @@ -28,6 +27,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +72,8 @@ void handlingError(HttpServer httpServer) throws Exception { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } - @ParameterizedHttpServerTest // SPR-15560 + @ParameterizedHttpServerTest + // SPR-15560 void emptyPathSegments(HttpServer httpServer) throws Exception { startServer(httpServer); @@ -86,7 +87,7 @@ void emptyPathSegments(HttpServer httpServer) throws Exception { // but an application can apply CompactPathRule via RewriteHandler: // https://www.eclipse.org/jetty/documentation/jetty-11/programming_guide.php - HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer + HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer ? HttpStatus.BAD_REQUEST : HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(expectedStatus); diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index c4f24f69d18d..97c0c4252c07 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -19,7 +19,6 @@ import java.io.File; import java.net.URI; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Mono; import org.springframework.core.io.ClassPathResource; @@ -29,6 +28,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.web.client.RestTemplate; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -52,7 +56,7 @@ protected HttpHandler createHttpHandler() { @ParameterizedHttpServerTest void zeroCopy(HttpServer httpServer) throws Exception { assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer || httpServer instanceof JettyCoreHttpServer, - "Zero-copy does not support Servlet"); + "Zero-copy does not support Servlet"); startServer(httpServer); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index c40dc6fa5538..a68d384f20d5 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -20,6 +20,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; + import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; /** @@ -31,6 +32,7 @@ public class JettyCoreHttpServer extends AbstractHttpServer { private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove + private Server jettyServer; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index bb22457df381..07e7a1b61ec3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -28,6 +28,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.context.ApplicationContext; import org.springframework.context.i18n.LocaleContext; import org.springframework.core.ResolvableType; @@ -54,8 +57,6 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Default {@link ServerRequest.Builder} implementation. @@ -298,8 +299,7 @@ public Flux getBody() { } @Override - public T getNativeRequest() - { + public T getNativeRequest() { return null; } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index b8f38256495b..a5b2fdceffb7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.core.OpCode; + import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -55,6 +56,7 @@ public class JettyWebSocketHandlerAdapter { private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); private final WebSocketHandler delegateHandler; + private final Function sessionFactory; @Nullable diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java index 41832cc62a7a..52d77f750aa8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java @@ -27,6 +27,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + import org.springframework.context.Lifecycle; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -51,7 +53,6 @@ import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; -import reactor.core.publisher.Mono; /** * {@code WebSocketService} implementation that handles a WebSocket HTTP diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 66cab235901b..509981c6dced 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -28,6 +28,8 @@ import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; import org.eclipse.jetty.websocket.server.WebSocketCreator; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -41,7 +43,6 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; /** * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 Core. @@ -84,32 +85,31 @@ public Mono upgrade( // Trigger WebFlux preCommit actions before upgrade return exchange.getResponse().setComplete() .then(Mono.deferContextual(contextView -> { - JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( - ContextWebSocketHandler.decorate(handler, contextView), - session -> new JettyWebSocketSession(session, handshakeInfo, factory)); - - WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> - { - if (subProtocol != null) - upgradeResponse.setAcceptedSubProtocol(subProtocol); - return adapter; - }; - - Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); - ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); - try - { - if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) - throw new WebSocketException("request could not be upgraded to websocket"); - } - catch (WebSocketException e) - { - callback.failed(e); - } - - return mono; - })); + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new JettyWebSocketSession(session, handshakeInfo, factory)); + + WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> { + if (subProtocol != null) { + upgradeResponse.setAcceptedSubProtocol(subProtocol); + } + return adapter; + }; + + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); + ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); + try { + if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) { + throw new WebSocketException("request could not be upgraded to websocket"); + } + } + catch (WebSocketException ex) { + callback.failed(ex); + } + + return mono; + })); } private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index 82711c47262c..46d8b7090365 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -25,6 +25,8 @@ import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.websocket.api.Configurable; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -38,7 +40,6 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; /** * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 EE10. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java index 91104f2ed8e1..06c01fbfef69 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java @@ -16,11 +16,13 @@ package org.springframework.web.reactive.result.method.annotation; +import java.util.stream.Stream; + import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; - import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,9 +33,13 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; - -import java.util.stream.Stream; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 0328e05ae92d..c7734d7f39e6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -52,6 +51,13 @@ import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -130,7 +136,8 @@ void sseAsEvent(HttpServer httpServer, ClientHttpConnector connector) throws Exc .uri("/event") .accept(TEXT_EVENT_STREAM) .retrieve() - .bodyToFlux(new ParameterizedTypeReference<>() {}); + .bodyToFlux(new ParameterizedTypeReference<>() { + }); verifyPersonEvents(result); } @@ -143,21 +150,22 @@ void sseAsEventWithoutAcceptHeader(HttpServer httpServer, ClientHttpConnector co .uri("/event") .accept(TEXT_EVENT_STREAM) .retrieve() - .bodyToFlux(new ParameterizedTypeReference<>() {}); + .bodyToFlux(new ParameterizedTypeReference<>() { + }); verifyPersonEvents(result); } private void verifyPersonEvents(Flux> result) { StepVerifier.create(result) - .consumeNextWith( event -> { + .consumeNextWith(event -> { assertThat(event.id()).isEqualTo("0"); assertThat(event.data()).isEqualTo(new Person("foo 0")); assertThat(event.comment()).isEqualTo("bar 0"); assertThat(event.event()).isNull(); assertThat(event.retry()).isNull(); }) - .consumeNextWith( event -> { + .consumeNextWith(event -> { assertThat(event.id()).isEqualTo("1"); assertThat(event.data()).isEqualTo(new Person("foo 1")); assertThat(event.comment()).isEqualTo("bar 1"); @@ -169,7 +177,8 @@ private void verifyPersonEvents(Flux> result) { } @ParameterizedSseTest // SPR-16494 - @Disabled // https://github.com/reactor/reactor-netty/issues/283 + @Disabled + // https://github.com/reactor/reactor-netty/issues/283 void serverDetectsClientDisconnect(HttpServer httpServer, ClientHttpConnector connector) throws Exception { assumeTrue(httpServer instanceof ReactorHttpServer); @@ -297,21 +306,21 @@ public String toString() { static Stream arguments() { return Stream.of( - args(new JettyHttpServer(), new ReactorClientHttpConnector()), - args(new JettyHttpServer(), new JettyClientHttpConnector()), - args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), - args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), - args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), - args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), - args(new ReactorHttpServer(), new ReactorClientHttpConnector()), - args(new ReactorHttpServer(), new JettyClientHttpConnector()), - args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), - args(new TomcatHttpServer(), new ReactorClientHttpConnector()), - args(new TomcatHttpServer(), new JettyClientHttpConnector()), - args(new TomcatHttpServer(), new HttpComponentsClientHttpConnector()), - args(new UndertowHttpServer(), new ReactorClientHttpConnector()), - args(new UndertowHttpServer(), new JettyClientHttpConnector()), - args(new UndertowHttpServer(), new HttpComponentsClientHttpConnector()) + args(new JettyHttpServer(), new ReactorClientHttpConnector()), + args(new JettyHttpServer(), new JettyClientHttpConnector()), + args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), + args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), + args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), + args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), + args(new ReactorHttpServer(), new ReactorClientHttpConnector()), + args(new ReactorHttpServer(), new JettyClientHttpConnector()), + args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), + args(new TomcatHttpServer(), new ReactorClientHttpConnector()), + args(new TomcatHttpServer(), new JettyClientHttpConnector()), + args(new TomcatHttpServer(), new HttpComponentsClientHttpConnector()), + args(new UndertowHttpServer(), new ReactorClientHttpConnector()), + args(new UndertowHttpServer(), new JettyClientHttpConnector()), + args(new UndertowHttpServer(), new HttpComponentsClientHttpConnector()) ); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index afa859b6cbf3..c189651e433e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -20,6 +20,9 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; @@ -28,8 +31,6 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Mono; /** * Response that subscribes to the writes source but never posts demand and also @@ -116,8 +117,7 @@ public HttpHeaders getHeaders() { } @Override - public T getNativeResponse() - { + public T getNativeResponse() { return null; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index f991291c13f7..7b176affcf61 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,6 +31,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.xnio.OptionMap; +import org.xnio.Xnio; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple3; + import org.springframework.context.ApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -61,11 +67,6 @@ import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; -import org.xnio.OptionMap; -import org.xnio.Xnio; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple3; /** * Base class for reactive WebSocket integration tests. Subclasses must implement From 67622d1dc6a6b3a93ca4572571b2dbeaa1bd481f Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 18:58:19 +0900 Subject: [PATCH 033/146] WIP on fixing leaks --- .../reactive/JettyCoreServerHttpRequest.java | 2 +- .../reactive/JettyRetainedDataBuffer.java | 27 +++++++++++-------- .../bootstrap/JettyCoreHttpServer.java | 14 +++++++--- .../reactive/bootstrap/JettyHttpServer.java | 4 ++- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 02c911e1a58a..d5ea2e553d11 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -110,7 +110,7 @@ public Flux getBody() { } private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 77af0e288986..e19a9bf686b2 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -20,9 +20,10 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntPredicate; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; import org.springframework.core.io.buffer.DataBuffer; @@ -37,26 +38,30 @@ * @since 6.1.4 */ public class JettyRetainedDataBuffer implements PooledDataBuffer { - private final Retainable retainable; + + private final Content.Chunk chunk; private final DataBuffer dataBuffer; - private final AtomicBoolean allocated = new AtomicBoolean(true); + private final AtomicInteger allocated = new AtomicInteger(1); + - public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { - this.dataBuffer = dataBuffer; - this.retainable = retainable; - this.retainable.retain(); + public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { + this.chunk = chunk; + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? + this.chunk.retain(); } @Override public boolean isAllocated() { - return this.allocated.get(); + return this.allocated.get() >= 1; } @Override public PooledDataBuffer retain() { - this.retainable.retain(); + if (this.allocated.updateAndGet(c -> c >= 1 ? c + 1 : c) < 1) { + throw new IllegalStateException("released"); + } return this; } @@ -67,8 +72,8 @@ public PooledDataBuffer touch(Object hint) { @Override public boolean release() { - if (this.retainable.release()) { - this.allocated.set(false); + if (this.allocated.decrementAndGet() == 0) { + this.chunk.release(); return true; } return false; diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index a68d384f20d5..003ad2ab6490 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -64,6 +64,7 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() { + boolean wasRunning = this.jettyServer.isRunning(); try { this.jettyServer.stop(); } @@ -72,15 +73,20 @@ protected void stopInternal() { } // TODO remove this or make debug only - this.byteBufferPool.dumpLeaks(); - if (!this.byteBufferPool.getLeaks().isEmpty()) - throw new IllegalStateException("LEAKS"); + if (wasRunning) { + if (!this.byteBufferPool.getLeaks().isEmpty()) { + System.err.println("Leaks:\n" + this.byteBufferPool.dumpLeaks()); + throw new IllegalStateException("LEAKS"); + } + } } @Override protected void resetInternal() { try { - stopInternal(); + if (this.jettyServer.isRunning()) { + stopInternal(); + } this.jettyServer.destroy(); } finally { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java index 08b7d32b3962..12878528ff1f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java @@ -79,7 +79,9 @@ protected void stopInternal() throws Exception { @Override protected void resetInternal() { try { - this.jettyServer.stop(); + if (this.jettyServer.isRunning()) { + this.jettyServer.stop(); + } } catch (Exception ex) { throw new IllegalStateException(ex); From 0530dd7e5d6775d7d072e4ca5ae14244926a3398 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 19:31:06 +0900 Subject: [PATCH 034/146] WIP on fixing leaks --- .../server/reactive/JettyCoreServerHttpRequest.java | 11 +++++++++-- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d5ea2e553d11..bb597b3341ad 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -34,6 +34,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -106,13 +107,19 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap).doOnNext(this::release); } - private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { + private DataBuffer wrap(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } + private void release(DataBuffer dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer pooled) { + pooled.release(); + } + } + @Override public String getId() { return this.request.getId(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index e19a9bf686b2..a05bf8e4b277 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; +import org.eclipse.jetty.util.BufferUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; From a5b7d219d00cea99b94ae2b6a0db657a77b082cb Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 22:06:22 +0900 Subject: [PATCH 035/146] WIP on fixing tests --- .../http/server/reactive/JettyRetainedDataBuffer.java | 3 ++- spring-webflux/spring-webflux.gradle | 2 +- .../method/annotation/MultipartWebClientIntegrationTests.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index a05bf8e4b277..ac61e231bb91 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -49,7 +49,8 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { this.chunk = chunk; - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? + // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid copy and double slice? this.chunk.retain(); } diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 396eba3dfe69..d0f27d258653 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -27,7 +27,7 @@ dependencies { optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } - optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java index cc0fb65e35d7..a6f6d896c153 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java @@ -169,6 +169,7 @@ void filePartsMono(HttpServer httpServer) throws Exception { @ParameterizedHttpServerTest void transferTo(HttpServer httpServer) throws Exception { // TODO Determine why Undertow fails: https://github.com/spring-projects/spring-framework/issues/25310 + // Jetty is also failing this test in https://github.com/spring-projects/spring-framework/pull/32097 assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails with transferTo"); startServer(httpServer); From f25837c1f606878a10eb6d3acf9ebcbf6f3465f5 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 10:32:51 +0900 Subject: [PATCH 036/146] Updated to jetty 12.0.6 --- framework-platform/framework-platform.gradle | 4 +-- .../reactive/JettyRetainedDataBuffer.java | 4 +-- .../http/support/JettyHeadersAdapter.java | 25 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 3333f3a9e957..73d04f3c25d6 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -16,8 +16,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.17")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.25.2")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.5")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.5")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.6")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.6")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index ac61e231bb91..4b3ca53e999c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -49,8 +49,8 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { this.chunk = chunk; - // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid copy and double slice? + // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); // TODO this copy avoids multipart bugs + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? this.chunk.retain(); } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index e57bd76e8b2c..83c7b9fff490 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -162,21 +162,20 @@ public List get(Object key) { public List put(String key, List value) { HttpFields.Mutable mutableHttpFields = mutableFields(); List oldValues = get(key); - switch (value.size()) { - case 0 -> { - if (oldValues != null) { - mutableHttpFields.remove(key); - } + + if (oldValues == null) { + switch (value.size()) { + case 0 -> {} + case 1 -> mutableHttpFields.add(key, value.get(0)); + default -> mutableHttpFields.add(key, value); } - case 1 -> { - if (oldValues == null) { - mutableHttpFields.add(key, value.get(0)); - } - else { - mutableHttpFields.put(key, value.get(0)); - } + } + else { + switch (value.size()) { + case 0 -> mutableHttpFields.remove(key); + case 1 -> mutableHttpFields.put(key, value.get(0)); + default -> mutableHttpFields.put(key, value); } - default -> mutableHttpFields.put(key, value); } return oldValues; } From 7cf32145f444d91138a4877d01f1c437fb2881d1 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 11:30:33 +0900 Subject: [PATCH 037/146] Move all response cookies to multiMap for possible mutation by spring --- .../reactive/JettyCoreServerHttpResponse.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 09261ef76c52..5d533cf196a5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -24,11 +24,13 @@ import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; @@ -131,7 +133,7 @@ public Mono writeWith(Path file, long position, long count) { mono = Mono.fromFuture(callback); try { // TODO: Why does intellij warn about possible blocking call? - // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel + // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } @@ -159,7 +161,6 @@ private Mono ensureCommitted() { private void writeCookies() { if (this.cookies != null) { - // TODO: are we doubling up on cookies already existing in response? this.cookies.values().stream() .flatMap(List::stream) .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); @@ -227,18 +228,12 @@ public boolean setRawStatusCode(@Nullable Integer value) { @Override public MultiValueMap getCookies() { - if (this.cookies == null) { - initializeCookies(); - } - return this.cookies; + return initializeCookies(); } @Override public void addCookie(ResponseCookie cookie) { - if (this.cookies == null) { - initializeCookies(); - } - this.cookies.add(cookie.getName(), cookie); + initializeCookies().add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -247,13 +242,27 @@ public T getNativeResponse() { return (T) this.response; } - private void initializeCookies() { - this.cookies = new LinkedMultiValueMap<>(); - for (HttpField f : this.response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) { - this.cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + private LinkedMultiValueMap initializeCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap<>(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.cookies.add(responseCookie.getName(), responseCookie); + i.remove(); + } } } + return this.cookies; } private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { From c8c3734deb3fd27eadb2319d8f781c2e9d1f1e41 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 12:03:35 +0900 Subject: [PATCH 038/146] Use an atomicReference to release the last used databuffer after onNext has returned. --- .../server/reactive/JettyCoreServerHttpRequest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index bb597b3341ad..d1e5a7f99bbb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -107,14 +108,20 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap).doOnNext(this::release); + + // TODO find a better way to release after each onNext call to a subscriber + AtomicReference last = new AtomicReference<>(); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) + .map(this::wrap) + .doOnNext(db -> release(last.getAndSet(db))) + .doOnComplete(() -> release(last.getAndSet(null))); } private DataBuffer wrap(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } - private void release(DataBuffer dataBuffer) { + private static void release(DataBuffer dataBuffer) { if (dataBuffer instanceof PooledDataBuffer pooled) { pooled.release(); } From 74aa6cbe578b06e1135bd048923752e40af9063d Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 12:20:18 +0900 Subject: [PATCH 039/146] less allocations for releasing after onNext --- .../reactive/JettyCoreServerHttpRequest.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d1e5a7f99bbb..bf1f537a1d2b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -110,23 +110,31 @@ public Flux getBody() { // retained within a call to onNext. // TODO find a better way to release after each onNext call to a subscriber - AtomicReference last = new AtomicReference<>(); + DataBufferReleaser releaser = new DataBufferReleaser(); return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) .map(this::wrap) - .doOnNext(db -> release(last.getAndSet(db))) - .doOnComplete(() -> release(last.getAndSet(null))); + .doOnNext(releaser::onNext) + .doOnComplete(releaser::onComplete); } - private DataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); - } + private static class DataBufferReleaser { + private final AtomicReference last = new AtomicReference<>(); + + public void onNext(@Nullable DataBuffer dataBuffer) { + if (last.getAndSet(dataBuffer) instanceof PooledDataBuffer pooled) { + pooled.release(); + } + } - private static void release(DataBuffer dataBuffer) { - if (dataBuffer instanceof PooledDataBuffer pooled) { - pooled.release(); + public void onComplete() { + onNext(null); } } + private DataBuffer wrap(Content.Chunk chunk) { + return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); + } + @Override public String getId() { return this.request.getId(); From befdde435082524ae82d77018d56c4c72385f41c Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 1 Feb 2024 19:09:31 +1100 Subject: [PATCH 040/146] release DataBuffer from the JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpRequest.java | 29 ++----------------- .../reactive/JettyCoreServerHttpResponse.java | 8 +++-- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index bf1f537a1d2b..51fb3989d16a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -31,11 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -46,6 +42,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -106,29 +103,9 @@ public URI getURI() { @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be - // retained within a call to onNext. - - // TODO find a better way to release after each onNext call to a subscriber - DataBufferReleaser releaser = new DataBufferReleaser(); + // then wrapped as a Flux. return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) - .map(this::wrap) - .doOnNext(releaser::onNext) - .doOnComplete(releaser::onComplete); - } - - private static class DataBufferReleaser { - private final AtomicReference last = new AtomicReference<>(); - - public void onNext(@Nullable DataBuffer dataBuffer) { - if (last.getAndSet(dataBuffer) instanceof PooledDataBuffer pooled) { - pooled.release(); - } - } - - public void onComplete() { - onNext(null); - } + .map(this::wrap); } private DataBuffer wrap(Content.Chunk chunk) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 5d533cf196a5..fb68fc878ca8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,11 +42,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -56,6 +54,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -189,12 +189,14 @@ protected Action process() { @Override protected void onCompleteSuccess() { byteBufferIterator.close(); + DataBufferUtils.release(dataBuffer); callback.complete(null); } @Override protected void onCompleteFailure(Throwable cause) { byteBufferIterator.close(); + DataBufferUtils.release(dataBuffer); callback.failed(cause); } }.iterate(); From ee59264f10a910047364a99e88043c791ad83c2d Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 3 Feb 2024 09:03:02 +0100 Subject: [PATCH 041/146] fixed style --- .../http/server/reactive/JettyCoreServerHttpRequest.java | 3 ++- .../http/server/reactive/JettyCoreServerHttpResponse.java | 5 +++-- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 51fb3989d16a..d7c073d1496d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -30,6 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -42,7 +44,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index fb68fc878ca8..54df884063eb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,6 +42,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -54,8 +57,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 4b3ca53e999c..b35fabf017c3 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -25,7 +25,6 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; -import org.eclipse.jetty.util.BufferUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; From bbd2573babc577511d29a81e2fd70c701b92d389 Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Tue, 6 Feb 2024 10:40:52 +1000 Subject: [PATCH 042/146] fix build with jetty 12.0.7-SNAPSHOT Signed-off-by: Olivier Lamy --- build.gradle | 1 + framework-api/framework-api.gradle | 1 + framework-platform/framework-platform.gradle | 10 +++++++--- spring-jcl/spring-jcl.gradle | 2 ++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5a3e85c7aff5..cc223770af23 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" group = "org.springframework" repositories { + mavenLocal() mavenCentral() maven { url "https://repo.spring.io/milestone" diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index 016ca58a7a40..5cbe94dea6c2 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -8,6 +8,7 @@ description = "Spring Framework API Docs" apply from: "${rootDir}/gradle/publications.gradle" repositories { + mavenLocal() maven { url "https://repo.spring.io/release" } diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 73d04f3c25d6..c918fa5cef27 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -6,6 +6,10 @@ javaPlatform { allowDependencies() } +repositories { + mavenLocal() +} + dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.15.3")) api(platform("io.micrometer:micrometer-bom:1.12.2")) @@ -14,10 +18,10 @@ dependencies { api(platform("io.projectreactor:reactor-bom:2023.0.2")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.17")) - api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) + api(platform("org.apache.logging.log4j:log4j-bom:2.22.1")) api(platform("org.assertj:assertj-bom:3.25.2")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.6")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.6")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.7-SNAPSHOT")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7-SNAPSHOT")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle index d609737b2551..560d85324cb0 100644 --- a/spring-jcl/spring-jcl.gradle +++ b/spring-jcl/spring-jcl.gradle @@ -3,4 +3,6 @@ description = "Spring Commons Logging Bridge" dependencies { optional("org.apache.logging.log4j:log4j-api") optional("org.slf4j:slf4j-api") + optional("biz.aQute.bnd:biz.aQute.bnd.annotation:6.3.1") + optional("org.osgi:osgi.annotation:8.1.0") } From b1c778d6ad053bc515e172ccab4f2873d6c8a516 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 10:32:51 +0900 Subject: [PATCH 043/146] Updated to jetty 12.0.6 --- framework-platform/framework-platform.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index c918fa5cef27..b86a8ae12223 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -20,8 +20,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.17")) api(platform("org.apache.logging.log4j:log4j-bom:2.22.1")) api(platform("org.assertj:assertj-bom:3.25.2")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.7-SNAPSHOT")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7-SNAPSHOT")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.6")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.6")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) From 5f1712d4770360e67727fbf4f6b02b324d7ead3f Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:10:57 +1100 Subject: [PATCH 044/146] Implement a JettyWebSocketSession based on demand. Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpRequest.java | 1 - .../reactive/JettyCoreServerHttpResponse.java | 4 +- spring-webflux/spring-webflux.gradle | 1 + .../adapter/JettyWebSocketHandlerAdapter.java | 103 ++++++------ .../socket/adapter/JettyWebSocketSession.java | 150 ++++++++++++------ 5 files changed, 148 insertions(+), 111 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d7c073d1496d..59bf4d0d28c0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -53,7 +53,6 @@ * @author Greg Wilkins * @since 6.2 */ -// TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 54df884063eb..ced7f42c7ddd 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -65,7 +65,6 @@ * @author Lachlan Roberts * @since 6.2 */ -// TODO: extend AbstractServerHttpResponse for websocket. class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); @@ -133,8 +132,7 @@ public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { - // TODO: Why does intellij warn about possible blocking call? - // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel? + @SuppressWarnings("BlockingMethodInNonBlockingContext") SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index d0f27d258653..086953fc398b 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -28,6 +28,7 @@ dependencies { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-client") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index a5b2fdceffb7..9e99f3259f26 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -19,24 +19,20 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.function.Function; import java.util.function.IntPredicate; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.core.OpCode; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -51,15 +47,12 @@ * @author Rossen Stoyanchev * @since 5.0 */ -@WebSocket -public class JettyWebSocketHandlerAdapter { - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - +public class JettyWebSocketHandlerAdapter implements Session.Listener { private final WebSocketHandler delegateHandler; private final Function sessionFactory; - @Nullable + @SuppressWarnings("NotNullFieldNotInitialized") private JettyWebSocketSession delegateSession; public JettyWebSocketHandlerAdapter(WebSocketHandler handler, @@ -71,65 +64,63 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler handler, this.sessionFactory = sessionFactory; } - @OnWebSocketOpen + @Override public void onWebSocketOpen(Session session) { - this.delegateSession = this.sessionFactory.apply(session); + this.delegateSession = Objects.requireNonNull(this.sessionFactory.apply(session)); this.delegateHandler.handle(this.delegateSession) - .checkpoint(session.getUpgradeRequest().getRequestURI() + " [JettyWebSocketHandlerAdapter]") - .subscribe(this.delegateSession); + .subscribe(new Subscriber<>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void unused) { + } + + @Override + public void onError(Throwable t) { + delegateSession.onHandlerError(t); + } + + @Override + public void onComplete() { + delegateSession.onHandleComplete(); + } + }); } - @OnWebSocketMessage + @Override public void onWebSocketText(String message) { - if (this.delegateSession != null) { - byte[] bytes = message.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - } + byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketMessage + @Override public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { - if (this.delegateSession != null) { - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); - buffer = new JettyDataBuffer(buffer, callback); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - } - else { - callback.succeed(); - } + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); + buffer = new JettyDataBuffer(buffer, callback); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketFrame - public void onWebSocketFrame(Frame frame, Callback callback) { - if (this.delegateSession != null) { - if (OpCode.PONG == frame.getOpCode()) { - ByteBuffer byteBuffer = (frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD); - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); - buffer = new JettyDataBuffer(buffer, callback); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - return; - } - } - - callback.succeed(); + @Override + public void onWebSocketPong(ByteBuffer payload) { + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(BufferUtil.copy(payload)); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketClose + @Override public void onWebSocketClose(int statusCode, String reason) { - if (this.delegateSession != null) { - this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); - } + this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); } - @OnWebSocketError + @Override public void onWebSocketError(Throwable cause) { - if (this.delegateSession != null) { - this.delegateSession.handleError(cause); - } + this.delegateSession.handleError(cause); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index d6adca7e9298..c85a4fe523b0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -16,12 +16,15 @@ package org.springframework.web.reactive.socket.adapter; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicLong; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -36,13 +39,23 @@ /** * Spring {@link WebSocketSession} implementation that adapts to a Jetty - * WebSocket {@link org.eclipse.jetty.websocket.api.Session}. + * WebSocket {@link Session}. * * @author Violeta Georgieva * @author Rossen Stoyanchev * @since 5.0 */ -public class JettyWebSocketSession extends AbstractListenerWebSocketSession { +public class JettyWebSocketSession extends AbstractWebSocketSession { + + private final Flux flux; + private final AtomicLong requested = new AtomicLong(0); + + private final Sinks.One closeStatusSink = Sinks.one(); + @Nullable + private FluxSink sink; + + @Nullable + private final Sinks.Empty handlerCompletionSink; public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) { this(session, info, factory, null); @@ -51,52 +64,45 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory, @Nullable Sinks.Empty completionSink) { - super(session, ObjectUtils.getIdentityHexString(session), info, factory, completionSink); - // TODO: suspend causes failures if invoked at this stage - // suspendReceiving(); + super(session, ObjectUtils.getIdentityHexString(session), info, factory); + this.handlerCompletionSink = completionSink; + this.flux = Flux.create(emitter -> { + this.sink = emitter; + emitter.onRequest(n -> + { + requested.addAndGet(n); + tryDemand(); + }); + }); } - - @Override - protected boolean canSuspendReceiving() { - // Jetty 12 TODO: research suspend functionality in Jetty 12 - return false; + void handleMessage(WebSocketMessage.Type type, WebSocketMessage message) { + this.sink.next(message); + tryDemand(); } - @Override - protected void suspendReceiving() { + void handleError(Throwable ex) { } - @Override - protected void resumeReceiving() { + void handleClose(CloseStatus closeStatus) { + this.closeStatusSink.tryEmitValue(closeStatus); + this.sink.complete(); } - @Override - protected boolean sendMessage(WebSocketMessage message) throws IOException { - DataBuffer dataBuffer = message.getPayload(); - Session session = getDelegate(); - if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - getSendProcessor().setReadyToSend(false); - String text = dataBuffer.toString(StandardCharsets.UTF_8); - session.sendText(text, new SendProcessorCallback()); + void onHandlerError(Throwable ex) { + if (this.handlerCompletionSink != null) { + // Ignore result: can't overflow, ok if not first or no one listens + this.handlerCompletionSink.tryEmitError(ex); } - else { - if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - getSendProcessor().setReadyToSend(false); - } - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - switch (message.getType()) { - case BINARY -> session.sendBinary(byteBuffer, new SendProcessorCallback()); - case PING -> session.sendPing(byteBuffer, new SendProcessorCallback()); - case PONG -> session.sendPong(byteBuffer, new SendProcessorCallback()); - default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); - } - } - } + close(CloseStatus.SERVER_ERROR); + } + + void onHandleComplete() { + if (this.handlerCompletionSink != null) { + // Ignore result: can't overflow, ok if not first or no one listens + this.handlerCompletionSink.tryEmitEmpty(); } - return true; + close(); } @Override @@ -108,25 +114,67 @@ public boolean isOpen() { public Mono close(CloseStatus status) { Callback.Completable callback = new Callback.Completable(); getDelegate().close(status.getCode(), status.getReason(), callback); - return Mono.fromFuture(callback); } + @Override + public Mono closeStatus() { + return closeStatusSink.asMono(); + } - private final class SendProcessorCallback implements Callback { - - @Override - public void fail(Throwable x) { - getSendProcessor().cancel(); - getSendProcessor().onError(x); - } + @Override + public Flux receive() { + return flux; + } - @Override - public void succeed() { - getSendProcessor().setReadyToSend(true); - getSendProcessor().onWritePossible(); + private void tryDemand() + { + while (true) + { + long r = requested.get(); + if (r == 0) + return; + + // TODO: protect against readpending from multiple demand. + if (requested.compareAndSet(r, r - 1)) + { + getDelegate().demand(); + return; + } } + } + @Override + public Mono send(Publisher messages) { + return Flux.from(messages) + .flatMap(this::sendMessage, 1) + .then(); } + protected Mono sendMessage(WebSocketMessage message) { + + Callback.Completable completable = new Callback.Completable(); + + DataBuffer dataBuffer = message.getPayload(); + Session session = getDelegate(); + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + String text = dataBuffer.toString(StandardCharsets.UTF_8); + session.sendText(text, completable); + } + else { + // TODO: Ping and Pong message should combine payload into single buffer? + try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + switch (message.getType()) { + case BINARY -> session.sendBinary(byteBuffer, completable); + case PING -> session.sendPing(byteBuffer, completable); + case PONG -> session.sendPong(byteBuffer, completable); + default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + } + } + } + return Mono.fromFuture(completable); + } } From 700f8e65569ee65b5a2d600cf81278be8bcbd0b5 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:11:45 +1100 Subject: [PATCH 045/146] Add a Jetty implementation of the spring WebSocketClient interface. Signed-off-by: Lachlan Roberts --- .../socket/client/JettyWebSocketClient.java | 78 +++++++++++++++++++ ...ractReactiveWebSocketIntegrationTests.java | 2 + 2 files changed, 80 insertions(+) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java new file mode 100644 index 000000000000..9c784d99c266 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -0,0 +1,78 @@ +package org.springframework.web.reactive.socket.client; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.JettyUpgradeListener; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; + +public class JettyWebSocketClient implements WebSocketClient { + + private final org.eclipse.jetty.websocket.client.WebSocketClient client; + + public JettyWebSocketClient() + { + this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); + LifeCycle.start(this.client); + } + + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) + { + this.client = client; + } + + @Override + public Mono execute(URI url, WebSocketHandler handler) { + return execute(url, null, handler); + } + + @Override + public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandler handler) { + + ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); + upgradeRequest.setSubProtocols(handler.getSubProtocols()); + if (headers != null) + headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); + + AtomicReference handshakeInfo = new AtomicReference<>(); + JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { + @Override + public void onHandshakeResponse(Request request, Response response) { + String protocol = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); + HttpHeaders responseHeaders = new HttpHeaders(); + response.getHeaders().forEach(header -> responseHeaders.addAll(header.getName(), header.getValueList())); + handshakeInfo.set(new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol)); + } + }; + + Sinks.Empty completion = Sinks.empty(); + JettyWebSocketHandlerAdapter handlerAdapter = new JettyWebSocketHandlerAdapter(handler, session -> + new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); + try { + this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) + .whenComplete((session, throwable) -> { + if (throwable != null) + completion.tryEmitError(throwable); + }); + return completion.asMono(); + } + catch (IOException e) { + return Mono.error(e); + } + } +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 7b176affcf61..9392f0cca98e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -45,6 +45,7 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.socket.client.JettyWebSocketClient; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.TomcatWebSocketClient; import org.springframework.web.reactive.socket.client.UndertowWebSocketClient; @@ -92,6 +93,7 @@ static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), + new JettyWebSocketClient(), new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From 1c7935e598c2f1b31ce3151c62cbfff3b258f7f9 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:44:40 +1100 Subject: [PATCH 046/146] Prevent possible ReadPendingException from multiple demand. Signed-off-by: Lachlan Roberts --- .../adapter/JettyWebSocketHandlerAdapter.java | 6 +- .../socket/adapter/JettyWebSocketSession.java | 103 ++++++++++++------ 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 9e99f3259f26..33ad751a130a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -95,7 +95,7 @@ public void onWebSocketText(String message) { byte[] bytes = message.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override @@ -103,14 +103,14 @@ public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); buffer = new JettyDataBuffer(buffer, callback); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override public void onWebSocketPong(ByteBuffer payload) { DataBuffer buffer = this.delegateSession.bufferFactory().wrap(BufferUtil.copy(payload)); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index c85a4fe523b0..ba3d0b46182b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -18,8 +18,10 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.reactivestreams.Publisher; @@ -48,10 +50,12 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; - private final AtomicLong requested = new AtomicLong(0); - private final Sinks.One closeStatusSink = Sinks.one(); - @Nullable + private final Lock lock = new ReentrantLock(); + private long requested = 0; + private boolean awaitingDemand = false; + + @SuppressWarnings("NotNullFieldNotInitialized") private FluxSink sink; @Nullable @@ -70,15 +74,49 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.sink = emitter; emitter.onRequest(n -> { - requested.addAndGet(n); - tryDemand(); + boolean demand = false; + lock.lock(); + try + { + requested += n; + if (!awaitingDemand && requested > 0) { + requested--; + awaitingDemand = true; + demand = true; + } + } + finally { + lock.unlock(); + } + + if (demand) + getDelegate().demand(); }); }); } - void handleMessage(WebSocketMessage.Type type, WebSocketMessage message) { + void handleMessage(WebSocketMessage message) { this.sink.next(message); - tryDemand(); + + boolean demand = false; + lock.lock(); + try + { + if (!awaitingDemand) + throw new IllegalStateException(); + awaitingDemand = false; + if (requested > 0) { + requested--; + awaitingDemand = true; + demand = true; + } + } + finally { + lock.unlock(); + } + + if (demand) + getDelegate().demand(); } void handleError(Throwable ex) { @@ -127,23 +165,6 @@ public Flux receive() { return flux; } - private void tryDemand() - { - while (true) - { - long r = requested.get(); - if (r == 0) - return; - - // TODO: protect against readpending from multiple demand. - if (requested.compareAndSet(r, r - 1)) - { - getDelegate().demand(); - return; - } - } - } - @Override public Mono send(Publisher messages) { return Flux.from(messages) @@ -162,17 +183,31 @@ protected Mono sendMessage(WebSocketMessage message) { session.sendText(text, completable); } else { - // TODO: Ping and Pong message should combine payload into single buffer? - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - switch (message.getType()) { - case BINARY -> session.sendBinary(byteBuffer, completable); - case PING -> session.sendPing(byteBuffer, completable); - case PONG -> session.sendPong(byteBuffer, completable); - default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + switch (message.getType()) { + case BINARY -> + { + try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + session.sendBinary(byteBuffer, completable); + } } } + case PING -> + { + // Maximum size of Control frame payload is 125, per RFC 6455. + ByteBuffer buffer = BufferUtil.allocate(125); + dataBuffer.toByteBuffer(buffer); + session.sendPing(buffer, completable); + } + case PONG -> + { + // Maximum size of Control frame payload is 125, per RFC 6455. + ByteBuffer buffer = BufferUtil.allocate(125); + dataBuffer.toByteBuffer(buffer); + session.sendPong(buffer, completable); + } + default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); } } return Mono.fromFuture(completable); From bbd4e68e770ca747873a4988528949a91d908208 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 7 Feb 2024 00:23:20 +1100 Subject: [PATCH 047/146] Update the JettyWebSocketHandlerAdapter as a Session.Listener Signed-off-by: Lachlan Roberts --- .../jetty/JettyWebSocketHandlerAdapter.java | 91 +++++++++---------- .../jetty/JettyRequestUpgradeStrategy.java | 2 +- 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index b996b920e1c3..c4576b31f43f 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -20,16 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.core.OpCode; import org.springframework.util.Assert; import org.springframework.web.socket.BinaryMessage; @@ -45,17 +38,13 @@ * @author Rossen Stoyanchev * @since 4.0 */ -@WebSocket -public class JettyWebSocketHandlerAdapter { - - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - +public class JettyWebSocketHandlerAdapter implements Session.Listener { private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class); - private final WebSocketHandler webSocketHandler; - private final JettyWebSocketSession wsSession; + @SuppressWarnings("NotNullFieldNotInitialized") + private Session nativeSession; public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { @@ -66,68 +55,57 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebS } - @OnWebSocketOpen + @Override public void onWebSocketOpen(Session session) { try { + this.nativeSession = session; this.wsSession.initializeNativeSession(session); this.webSocketHandler.afterConnectionEstablished(this.wsSession); + this.nativeSession.demand(); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketMessage + @Override public void onWebSocketText(String payload) { TextMessage message = new TextMessage(payload); try { this.webSocketHandler.handleMessage(this.wsSession, message); + this.nativeSession.demand(); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketMessage + @Override public void onWebSocketBinary(ByteBuffer payload, Callback callback) { - BinaryMessage message = new BinaryMessage(copyByteBuffer(payload), true); + BinaryMessage message = new BinaryMessage(BufferUtil.copy(payload), true); + callback.succeed(); try { this.webSocketHandler.handleMessage(this.wsSession, message); - callback.succeed(); + this.nativeSession.demand(); } catch (Exception ex) { - callback.fail(ex); - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketFrame - public void onWebSocketFrame(Frame frame, Callback callback) { - if (OpCode.PONG == frame.getOpCode()) { - ByteBuffer payload = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; - PongMessage message = new PongMessage(copyByteBuffer(payload)); - try { - this.webSocketHandler.handleMessage(this.wsSession, message); - callback.succeed(); - } - catch (Exception ex) { - callback.fail(ex); - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); - } + @Override + public void onWebSocketPong(ByteBuffer payload) { + PongMessage message = new PongMessage(BufferUtil.copy(payload)); + try { + this.webSocketHandler.handleMessage(this.wsSession, message); + this.nativeSession.demand(); } - else { - callback.succeed(); + catch (Exception ex) { + tryCloseWithError(ex); } } - private static ByteBuffer copyByteBuffer(ByteBuffer src) { - ByteBuffer dest = ByteBuffer.allocate(src.remaining()); - dest.put(src); - dest.flip(); - return dest; - } - - @OnWebSocketClose + @Override public void onWebSocketClose(int statusCode, String reason) { CloseStatus closeStatus = new CloseStatus(statusCode, reason); try { @@ -135,18 +113,31 @@ public void onWebSocketClose(int statusCode, String reason) { } catch (Exception ex) { if (logger.isWarnEnabled()) { - logger.warn("Unhandled exception after connection closed for " + this, ex); + logger.warn("Unhandled exception from afterConnectionClosed for " + this, ex); } } } - @OnWebSocketError + @Override public void onWebSocketError(Throwable cause) { try { this.webSocketHandler.handleTransportError(this.wsSession, cause); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + if (logger.isWarnEnabled()) { + logger.warn("Unhandled exception from handleTransportError for " + this, ex); + } + } + } + + private void tryCloseWithError(Throwable t) { + + if (nativeSession.isOpen()) { + ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); + } + else { + // Session might be O-SHUT waiting for response close frame, so abort to close the connection. + nativeSession.disconnect(); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java index 026f9af4fd73..2ed4542e3111 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java @@ -45,7 +45,7 @@ import org.springframework.web.socket.server.RequestUpgradeStrategy; /** - * A {@link RequestUpgradeStrategy} for Jetty 11. + * A {@link RequestUpgradeStrategy} for Jetty 12 EE10. * * @author Rossen Stoyanchev * @since 5.3.4 From 1f09a94eecbd86516376f75c6f32d22922f17617 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 7 Feb 2024 01:07:38 +1100 Subject: [PATCH 048/146] fixes for checkstyle Signed-off-by: Lachlan Roberts --- .../adapter/JettyWebSocketHandlerAdapter.java | 3 +- .../socket/adapter/JettyWebSocketSession.java | 55 +++++++++---------- .../socket/client/JettyWebSocketClient.java | 32 ++++++++--- .../jetty/JettyWebSocketHandlerAdapter.java | 6 +- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 33ad751a130a..6f42e85d7ec2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -26,7 +26,6 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -40,7 +39,7 @@ import org.springframework.web.reactive.socket.WebSocketMessage.Type; /** - * Jetty {@link WebSocket @WebSocket} handler that delegates events to a + * Jetty {@link org.eclipse.jetty.websocket.api.Session.Listener} handler that delegates events to a * reactive {@link WebSocketHandler} and its session. * * @author Violeta Georgieva diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index ba3d0b46182b..2f1ff6728e2f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -72,25 +72,24 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.handlerCompletionSink = completionSink; this.flux = Flux.create(emitter -> { this.sink = emitter; - emitter.onRequest(n -> - { + emitter.onRequest(n -> { boolean demand = false; - lock.lock(); - try - { - requested += n; - if (!awaitingDemand && requested > 0) { - requested--; - awaitingDemand = true; + this.lock.lock(); + try { + this.requested += n; + if (!this.awaitingDemand && this.requested > 0) { + this.requested--; + this.awaitingDemand = true; demand = true; } } finally { - lock.unlock(); + this.lock.unlock(); } - if (demand) + if (demand) { getDelegate().demand(); + } }); }); } @@ -99,24 +98,25 @@ void handleMessage(WebSocketMessage message) { this.sink.next(message); boolean demand = false; - lock.lock(); - try - { - if (!awaitingDemand) + this.lock.lock(); + try { + if (!this.awaitingDemand) { throw new IllegalStateException(); - awaitingDemand = false; - if (requested > 0) { - requested--; - awaitingDemand = true; + } + this.awaitingDemand = false; + if (this.requested > 0) { + this.requested--; + this.awaitingDemand = true; demand = true; } } finally { - lock.unlock(); + this.lock.unlock(); } - if (demand) + if (demand) { getDelegate().demand(); + } } void handleError(Throwable ex) { @@ -157,12 +157,12 @@ public Mono close(CloseStatus status) { @Override public Mono closeStatus() { - return closeStatusSink.asMono(); + return this.closeStatusSink.asMono(); } @Override public Flux receive() { - return flux; + return this.flux; } @Override @@ -184,8 +184,7 @@ protected Mono sendMessage(WebSocketMessage message) { } else { switch (message.getType()) { - case BINARY -> - { + case BINARY -> { try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { while (iterator.hasNext()) { ByteBuffer byteBuffer = iterator.next(); @@ -193,15 +192,13 @@ protected Mono sendMessage(WebSocketMessage message) { } } } - case PING -> - { + case PING -> { // Maximum size of Control frame payload is 125, per RFC 6455. ByteBuffer buffer = BufferUtil.allocate(125); dataBuffer.toByteBuffer(buffer); session.sendPing(buffer, completable); } - case PONG -> - { + case PONG -> { // Maximum size of Control frame payload is 125, per RFC 6455. ByteBuffer buffer = BufferUtil.allocate(125); dataBuffer.toByteBuffer(buffer); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 9c784d99c266..c47e5fc8d4d3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.reactive.socket.client; import java.io.IOException; @@ -25,14 +41,12 @@ public class JettyWebSocketClient implements WebSocketClient { private final org.eclipse.jetty.websocket.client.WebSocketClient client; - public JettyWebSocketClient() - { + public JettyWebSocketClient() { this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); LifeCycle.start(this.client); } - public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) - { + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { this.client = client; } @@ -46,8 +60,9 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); upgradeRequest.setSubProtocols(handler.getSubProtocols()); - if (headers != null) + if (headers != null) { headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); + } AtomicReference handshakeInfo = new AtomicReference<>(); JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { @@ -66,13 +81,14 @@ public void onHandshakeResponse(Request request, Response response) { try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) .whenComplete((session, throwable) -> { - if (throwable != null) + if (throwable != null) { completion.tryEmitError(throwable); + } }); return completion.asMono(); } - catch (IOException e) { - return Mono.error(e); + catch (IOException ex) { + return Mono.error(ex); } } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index c4576b31f43f..959ab677f1be 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -33,7 +33,7 @@ import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator; /** - * Adapts {@link WebSocketHandler} to the Jetty WebSocket API. + * Adapts {@link WebSocketHandler} to the Jetty WebSocket API {@link org.eclipse.jetty.websocket.api.Session.Listener}. * * @author Rossen Stoyanchev * @since 4.0 @@ -132,12 +132,12 @@ public void onWebSocketError(Throwable cause) { private void tryCloseWithError(Throwable t) { - if (nativeSession.isOpen()) { + if (this.nativeSession.isOpen()) { ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); } else { // Session might be O-SHUT waiting for response close frame, so abort to close the connection. - nativeSession.disconnect(); + this.nativeSession.disconnect(); } } From 08e91774e96d984f33171d3c4cd6cc6b8c12b23e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 12 Feb 2024 21:50:19 +1100 Subject: [PATCH 049/146] Ensure DataBuffer is processed correctly for BINARY messages. Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 1 + .../socket/adapter/JettyWebSocketSession.java | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index ced7f42c7ddd..bb6736b1fe90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -132,6 +132,7 @@ public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { + // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 2f1ff6728e2f..68c0ef982c6f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -22,6 +22,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.reactivestreams.Publisher; @@ -175,7 +176,6 @@ public Mono send(Publisher messages) { protected Mono sendMessage(WebSocketMessage message) { Callback.Completable completable = new Callback.Completable(); - DataBuffer dataBuffer = message.getPayload(); Session session = getDelegate(); if (WebSocketMessage.Type.TEXT.equals(message.getType())) { @@ -185,12 +185,32 @@ protected Mono sendMessage(WebSocketMessage message) { else { switch (message.getType()) { case BINARY -> { - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - session.sendBinary(byteBuffer, completable); + @SuppressWarnings("resource") + DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers(); + new IteratingCallback() { + @Override + protected Action process() { + if (!iterator.hasNext()) + return Action.SUCCEEDED; + + ByteBuffer buffer = iterator.next(); + boolean last = iterator.hasNext(); + session.sendPartialBinary(buffer, last, Callback.from(this::succeeded, this::failed)); + return Action.SCHEDULED; } - } + + @Override + protected void onCompleteSuccess() { + iterator.close(); + completable.complete(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) { + iterator.close(); + completable.completeExceptionally(cause); + } + }.iterate(); } case PING -> { // Maximum size of Control frame payload is 125, per RFC 6455. From 26cac28a8382782e7f8d821f303e5bffc390f67a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 13 Feb 2024 13:06:54 +1100 Subject: [PATCH 050/146] Wait for close of Session before calling handlerCompletionSink Signed-off-by: Lachlan Roberts --- framework-platform/framework-platform.gradle | 4 +- .../socket/adapter/JettyWebSocketSession.java | 44 ++++++++++++++----- .../socket/client/JettyWebSocketClient.java | 8 ++++ ...ractReactiveWebSocketIntegrationTests.java | 4 +- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index b86a8ae12223..c918fa5cef27 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -20,8 +20,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.17")) api(platform("org.apache.logging.log4j:log4j-bom:2.22.1")) api(platform("org.assertj:assertj-bom:3.25.2")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.6")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.6")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.7-SNAPSHOT")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7-SNAPSHOT")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 68c0ef982c6f..8565bea6f305 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -128,20 +129,41 @@ void handleClose(CloseStatus closeStatus) { this.sink.complete(); } - void onHandlerError(Throwable ex) { - if (this.handlerCompletionSink != null) { - // Ignore result: can't overflow, ok if not first or no one listens - this.handlerCompletionSink.tryEmitError(ex); - } - close(CloseStatus.SERVER_ERROR); + void onHandlerError(Throwable error) { + getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), new Callback() { + @Override + public void succeed() { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + } + + @Override + public void fail(Throwable ex) { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + error.addSuppressed(ex); + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + } + }); } void onHandleComplete() { - if (this.handlerCompletionSink != null) { - // Ignore result: can't overflow, ok if not first or no one listens - this.handlerCompletionSink.tryEmitEmpty(); - } - close(); + getDelegate().close(StatusCode.NORMAL, null, new Callback() { + @Override + public void succeed() { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + } + + @Override + public void fail(Throwable ex) { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + } + }); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index c47e5fc8d4d3..73b24a7a9633 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -50,6 +50,14 @@ public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient c this.client = client; } + public void start() throws Exception { + this.client.start(); + } + + public void stop() throws Exception { + this.client.stop(); + } + @Override public Mono execute(URI url, WebSocketHandler handler) { return execute(url, null, handler); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 9392f0cca98e..5b9e6b9b4654 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -89,11 +89,13 @@ abstract class AbstractReactiveWebSocketIntegrationTests { @interface ParameterizedWebSocketTest { } + private static final JettyWebSocketClient jettyClient = new JettyWebSocketClient(); + static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), - new JettyWebSocketClient(), + jettyClient, new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From e2b452a9b8552f572580ae78846ece472e19e80a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 13 Feb 2024 13:10:55 +1100 Subject: [PATCH 051/146] fix checkstyle error Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 8565bea6f305..10f16c6722b3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -212,8 +212,9 @@ protected Mono sendMessage(WebSocketMessage message) { new IteratingCallback() { @Override protected Action process() { - if (!iterator.hasNext()) + if (!iterator.hasNext()) { return Action.SUCCEEDED; + } ByteBuffer buffer = iterator.next(); boolean last = iterator.hasNext(); From a0f4fe253c815fee8186438826d46d35eb270a1c Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 28 Feb 2024 10:42:58 +1100 Subject: [PATCH 052/146] Changes from review. Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 4 ++-- .../web/reactive/socket/client/JettyWebSocketClient.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 10f16c6722b3..90325cc44d3c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -225,13 +225,13 @@ protected Action process() { @Override protected void onCompleteSuccess() { iterator.close(); - completable.complete(null); + completable.succeed(); } @Override protected void onCompleteFailure(Throwable cause) { iterator.close(); - completable.completeExceptionally(cause); + completable.fail(cause); } }.iterate(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 73b24a7a9633..3a2b33668bfd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -78,7 +78,7 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl public void onHandshakeResponse(Request request, Response response) { String protocol = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); HttpHeaders responseHeaders = new HttpHeaders(); - response.getHeaders().forEach(header -> responseHeaders.addAll(header.getName(), header.getValueList())); + response.getHeaders().forEach(header -> responseHeaders.add(header.getName(), header.getValue())); handshakeInfo.set(new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol)); } }; @@ -89,6 +89,8 @@ public void onHandshakeResponse(Request request, Response response) { try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) .whenComplete((session, throwable) -> { + // Only fail the completion if we have an error + // as the JettyWebSocketSession will never be opened. if (throwable != null) { completion.tryEmitError(throwable); } From 65cf7387cbfa5e85cb1f3c3c224bfb7a312f2eff Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Mar 2024 10:05:16 +0100 Subject: [PATCH 053/146] updated to jetty 12.0.7 --- framework-platform/framework-platform.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index d3ab40385527..5cda75794f8a 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -20,8 +20,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.18")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.25.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.6")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.6")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.7")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) From 6e251899e50a9be301fda753788c5e9217a368ed Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Mar 2024 16:47:29 +0100 Subject: [PATCH 054/146] updated to jetty 12.0.7 --- .../reactive/bootstrap/JettyCoreHttpServer.java | 16 ++++++++++------ .../MultipartRouterFunctionIntegrationTests.java | 6 ++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 003ad2ab6490..ba1a085ebcec 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,6 +16,8 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -31,14 +33,16 @@ */ public class JettyCoreHttpServer extends AbstractHttpServer { - private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove + protected Log logger = LogFactory.getLog(getClass().getName()); - private Server jettyServer; + private ArrayByteBufferPool byteBufferPool; + private Server jettyServer; @Override protected void initServer() { - this.byteBufferPool = new ArrayByteBufferPool.Tracking(); + if (logger.isTraceEnabled()) + this.byteBufferPool = new ArrayByteBufferPool.Tracking(); this.jettyServer = new Server(null, null, byteBufferPool); ServerConnector connector = new ServerConnector(this.jettyServer); @@ -73,9 +77,9 @@ protected void stopInternal() { } // TODO remove this or make debug only - if (wasRunning) { - if (!this.byteBufferPool.getLeaks().isEmpty()) { - System.err.println("Leaks:\n" + this.byteBufferPool.dumpLeaks()); + if (wasRunning && this.byteBufferPool instanceof ArrayByteBufferPool.Tracking tracking) { + if (!tracking.getLeaks().isEmpty()) { + System.err.println("Leaks:\n" + tracking.dumpLeaks()); throw new IllegalStateException("LEAKS"); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index 1268d8a15ac2..d736cfae9079 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -49,11 +49,13 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** @@ -165,6 +167,10 @@ void proxy(HttpServer httpServer) throws Exception { assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails proxying requests"); startServer(httpServer); + // TODO For JettyCore this test passes, but calls demand on the request Flux after the handling cycle is + // complete, causing an exception to be logged; and leaks buffers that appear not be released by the + // test application. + Mono> result = webClient .post() .uri("http://localhost:" + this.port + "/proxy") From 34429fe5d56821bf9e8173b458d9142538715029 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Mar 2024 16:52:56 +0100 Subject: [PATCH 055/146] updated to jetty 12.0.7 --- build.gradle | 1 - framework-api/framework-api.gradle | 1 - framework-platform/framework-platform.gradle | 4 ---- 3 files changed, 6 deletions(-) diff --git a/build.gradle b/build.gradle index 1cab2de771b6..fb9774658382 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,6 @@ configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" group = "org.springframework" repositories { - mavenLocal() mavenCentral() maven { url "https://repo.spring.io/milestone" diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index 5cbe94dea6c2..016ca58a7a40 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -8,7 +8,6 @@ description = "Spring Framework API Docs" apply from: "${rootDir}/gradle/publications.gradle" repositories { - mavenLocal() maven { url "https://repo.spring.io/release" } diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 5cda75794f8a..504fe5d3bc13 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -6,10 +6,6 @@ javaPlatform { allowDependencies() } -repositories { - mavenLocal() -} - dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.15.3")) api(platform("io.micrometer:micrometer-bom:1.12.3")) From 5d5e46aad418fce92281252897af6aa01281bcc4 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 22 Mar 2024 11:13:23 +0100 Subject: [PATCH 056/146] fixed checkstyle --- .../function/MultipartRouterFunctionIntegrationTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index d736cfae9079..c4e650a39c74 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -49,13 +49,11 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** From 85505fe9d7c9c96fb918be0fc194c3a308009277 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 9 Apr 2024 14:51:17 +1000 Subject: [PATCH 057/146] Implement LifeCycle for JettyWebSocketClient Signed-off-by: Lachlan Roberts --- .../socket/client/JettyWebSocketClient.java | 19 +++++++++++-------- ...ractReactiveWebSocketIntegrationTests.java | 4 +--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 3a2b33668bfd..c5889b5abcdb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import org.springframework.context.Lifecycle; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; @@ -37,25 +38,27 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; -public class JettyWebSocketClient implements WebSocketClient { +public class JettyWebSocketClient implements WebSocketClient, Lifecycle { private final org.eclipse.jetty.websocket.client.WebSocketClient client; public JettyWebSocketClient() { this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); - LifeCycle.start(this.client); } - public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { - this.client = client; + @Override + public void start() { + LifeCycle.start(this.client); } - public void start() throws Exception { - this.client.start(); + @Override + public void stop() { + LifeCycle.stop(this.client); } - public void stop() throws Exception { - this.client.stop(); + @Override + public boolean isRunning() { + return false; } @Override diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 5b9e6b9b4654..9392f0cca98e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -89,13 +89,11 @@ abstract class AbstractReactiveWebSocketIntegrationTests { @interface ParameterizedWebSocketTest { } - private static final JettyWebSocketClient jettyClient = new JettyWebSocketClient(); - static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), - jettyClient, + new JettyWebSocketClient(), new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From cd9063b379d7b455daee793198f0fe9e29e24499 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 9 Apr 2024 15:09:53 +1000 Subject: [PATCH 058/146] additional changes from review Signed-off-by: Lachlan Roberts --- .../socket/adapter/JettyWebSocketSession.java | 51 +++++-------------- .../socket/client/JettyWebSocketClient.java | 15 +++--- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 90325cc44d3c..3db75639f49b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -55,7 +55,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private final Sinks.One closeStatusSink = Sinks.one(); private final Lock lock = new ReentrantLock(); private long requested = 0; - private boolean awaitingDemand = false; + private boolean awaitingMessage = false; @SuppressWarnings("NotNullFieldNotInitialized") private FluxSink sink; @@ -79,9 +79,9 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.lock.lock(); try { this.requested += n; - if (!this.awaitingDemand && this.requested > 0) { + if (!this.awaitingMessage && this.requested > 0) { this.requested--; - this.awaitingDemand = true; + this.awaitingMessage = true; demand = true; } } @@ -102,13 +102,13 @@ void handleMessage(WebSocketMessage message) { boolean demand = false; this.lock.lock(); try { - if (!this.awaitingDemand) { + if (!this.awaitingMessage) { throw new IllegalStateException(); } - this.awaitingDemand = false; + this.awaitingMessage = false; if (this.requested > 0) { this.requested--; - this.awaitingDemand = true; + this.awaitingMessage = true; demand = true; } } @@ -130,40 +130,17 @@ void handleClose(CloseStatus closeStatus) { } void onHandlerError(Throwable error) { - getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), new Callback() { - @Override - public void succeed() { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); - } - } - - @Override - public void fail(Throwable ex) { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - error.addSuppressed(ex); - JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); - } - } - }); + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), Callback.NOOP); } void onHandleComplete() { - getDelegate().close(StatusCode.NORMAL, null, new Callback() { - @Override - public void succeed() { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); - } - } - - @Override - public void fail(Throwable ex) { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); - } - } - }); + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + getDelegate().close(StatusCode.NORMAL, null, Callback.NOOP); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index c5889b5abcdb..ae2b185fe4ea 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -43,7 +43,11 @@ public class JettyWebSocketClient implements WebSocketClient, Lifecycle { private final org.eclipse.jetty.websocket.client.WebSocketClient client; public JettyWebSocketClient() { - this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); + this(new org.eclipse.jetty.websocket.client.WebSocketClient()); + } + + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { + this.client = client; } @Override @@ -58,7 +62,7 @@ public void stop() { @Override public boolean isRunning() { - return false; + return this.client.isRunning(); } @Override @@ -91,12 +95,11 @@ public void onHandshakeResponse(Request request, Response response) { new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) - .whenComplete((session, throwable) -> { + .exceptionally((throwable) -> { // Only fail the completion if we have an error // as the JettyWebSocketSession will never be opened. - if (throwable != null) { - completion.tryEmitError(throwable); - } + completion.tryEmitError(throwable); + return null; }); return completion.asMono(); } From d6986ee9044c858df3692afa3d524a768572d24a Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 29 May 2024 14:26:30 +1000 Subject: [PATCH 059/146] Use abstract request/response classes --- .../reactive/JettyCoreServerHttpRequest.java | 122 ++++-------- .../reactive/JettyCoreServerHttpResponse.java | 179 +++++------------- .../reactive/ServerHttpRequestDecorator.java | 1 + .../reactive/ServerHttpResponseDecorator.java | 1 + .../server/DefaultServerRequestBuilder.java | 5 - .../adapter/JettyWebSocketHandlerAdapter.java | 3 +- 6 files changed, 87 insertions(+), 224 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 59bf4d0d28c0..34f30021e46f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -17,7 +17,6 @@ package org.springframework.http.server.reactive; import java.net.InetSocketAddress; -import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; @@ -37,7 +36,6 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -45,15 +43,13 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; - /** * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. * * @author Greg Wilkins * @since 6.2 */ -class JettyCoreServerHttpRequest implements ServerHttpRequest { +class JettyCoreServerHttpRequest extends AbstractServerHttpRequest { private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private static final MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); @@ -62,120 +58,80 @@ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final Request request; - private final HttpHeaders headers; - - private final RequestPath path; - - @Nullable - private URI uri; - - @Nullable - MultiValueMap queryParameters; - - @Nullable - private MultiValueMap cookies; - public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { + super(HttpMethod.valueOf(request.getMethod()), + request.getHttpURI().toURI(), + request.getContext().getContextPath(), + new HttpHeaders(new JettyHeadersAdapter(request.getHeaders()))); this.dataBufferFactory = dataBufferFactory; this.request = request; - this.headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - this.path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.request.getMethod()); - } - - @Override - public URI getURI() { - if (this.uri == null) { - this.uri = this.request.getHttpURI().toURI(); - } - return this.uri; } @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) - .map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::chunkToDataBuffer); } - private DataBuffer wrap(Content.Chunk chunk) { + private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } @Override - public String getId() { + protected String initId() { return this.request.getId(); } @Override - public RequestPath getPath() { - return this.path; - } + protected MultiValueMap initQueryParams() { + String query = this.request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) { + return EMPTY_QUERY; + } - @Override - public MultiValueMap getQueryParams() { - if (this.queryParameters == null) { - String query = this.request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) { - this.queryParameters = EMPTY_QUERY; - } - else { - this.queryParameters = new LinkedMultiValueMap<>(); - Matcher matcher = QUERY_PATTERN.matcher(query); - while (matcher.find()) { - String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - String eq = matcher.group(2); - String value = matcher.group(3); - value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - this.queryParameters.add(name, value); - } - } + MultiValueMap map = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + map.add(name, value); } - return this.queryParameters; + return map; } @Override - public MultiValueMap getCookies() { - if (this.cookies == null) { - List httpCookies = Request.getCookies(this.request); - if (httpCookies.isEmpty()) { - this.cookies = EMPTY_COOKIES; - } - else { - this.cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { - this.cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } - this.cookies = CollectionUtils.unmodifiableMultiValueMap(this.cookies); - } + protected MultiValueMap initCookies() { + List httpCookies = Request.getCookies(this.request); + if (httpCookies.isEmpty()) { + return EMPTY_COOKIES; + } + + MultiValueMap map =new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + map.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); } - return this.cookies; + + return map; } @Override + @Nullable public InetSocketAddress getLocalAddress() { - return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet - ? inet : null; + return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override + @Nullable public InetSocketAddress getRemoteAddress() { - return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet - ? inet : null; + return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override - public SslInfo getSslInfo() { + @Nullable + public SslInfo initSslInfo() { if (this.request.getConnectionMetaData().isSecure() && this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { return new SslInfo() { @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bb6736b1fe90..be668d03ee50 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,9 +26,6 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -46,7 +43,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -55,8 +51,6 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -65,72 +59,73 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { - private final AtomicBoolean committed = new AtomicBoolean(false); - - private final List>> commitActions = new CopyOnWriteArrayList<>(); +class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { private final Response response; - private final HttpHeaders headers; - - @Nullable - private LinkedMultiValueMap cookies; - public JettyCoreServerHttpResponse(Response response) { - this.response = response; - this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + this(null, response); } - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public DataBufferFactory bufferFactory() { - return DefaultDataBufferFactory.sharedInstance; - } - - @Override - public void beforeCommit(Supplier> action) { - this.commitActions.add(action); - } + public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { + super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, + new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); + this.response = response; - @Override - public boolean isCommitted() { - return this.committed.get(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.addCookie(responseCookie); + i.remove(); + } + } } @Override - public Mono writeWith(Publisher body) { + protected Mono writeWithInternal(Publisher body) { return Flux.from(body) .flatMap(this::sendDataBuffer, 1) .then(); } @Override - public Mono writeAndFlushWith(Publisher> body) { + protected Mono writeAndFlushWithInternal(Publisher> body) { return Flux.from(body) .flatMap(this::writeWith, 1) .then(); } @Override - public Mono setComplete() { - Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; + protected void applyStatusCode() { + HttpStatusCode status = getStatusCode(); + this.response.setStatus(status == null ? 0 : status.value()); } @Override - public Mono writeWith(Path file, long position, long count) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> writeWith(file, position, count))); - } + protected void applyHeaders() { + + } + + @Override + protected void applyCookies() { + this.getCookies().values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); + } + @Override + public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); - mono = Mono.fromFuture(callback); + Mono mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -140,39 +135,10 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return mono; - } - - @Nullable - private Mono ensureCommitted() { - if (this.committed.compareAndSet(false, true)) { - if (!this.commitActions.isEmpty()) { - return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::writeCookies)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); - } - - writeCookies(); - } - - return null; - } - - private void writeCookies() { - if (this.cookies != null) { - this.cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); - } + return doCommit(() -> mono); } private Mono sendDataBuffer(DataBuffer dataBuffer) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); - } - @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -201,41 +167,7 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return Mono.fromFuture(callback); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) { - return false; - } - this.response.setStatus(status.value()); - return true; - } - - @Override - public HttpStatusCode getStatusCode() { - int status = this.response.getStatus(); - return HttpStatusCode.valueOf(status == 0 ? 200 : status); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) { - return false; - } - this.response.setStatus(value); - return true; - } - - @Override - public MultiValueMap getCookies() { - return initializeCookies(); - } - - @Override - public void addCookie(ResponseCookie cookie) { - initializeCookies().add(cookie.getName(), cookie); + return doCommit(() -> Mono.fromFuture(callback)); } @SuppressWarnings("unchecked") @@ -244,33 +176,10 @@ public T getNativeResponse() { return (T) this.response; } - private LinkedMultiValueMap initializeCookies() { - if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(); - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.cookies.add(responseCookie.getName(), responseCookie); - i.remove(); - } - } - } - return this.cookies; - } - - private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public HttpResponseCookie(ResponseCookie responseCookie) { + public ResponseHttpCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 559ff010efa6..46100e7edbbc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -109,6 +109,7 @@ public SslInfo getSslInfo() { } @Override + @Nullable public T getNativeRequest() { return this.delegate.getNativeRequest(); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index d2ba47c747ed..3398a3416c0b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -124,6 +124,7 @@ public Mono setComplete() { } @Override + @Nullable public T getNativeResponse() { return getDelegate().getNativeResponse(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index 07e7a1b61ec3..a97fbd902563 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -297,11 +297,6 @@ public MultiValueMap getQueryParams() { public Flux getBody() { return this.body; } - - @Override - public T getNativeRequest() { - return null; - } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 6f42e85d7ec2..a8a747a6ddfe 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,6 +32,7 @@ import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -51,7 +52,7 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener { private final Function sessionFactory; - @SuppressWarnings("NotNullFieldNotInitialized") + @Nullable private JettyWebSocketSession delegateSession; public JettyWebSocketHandlerAdapter(WebSocketHandler handler, From 19215d4fba375cbec7978dde3abbb1f79515b681 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 29 May 2024 15:58:55 +1000 Subject: [PATCH 060/146] Suppress NullAway warnings for Jetty WebSocket classes Signed-off-by: Lachlan Roberts --- .../socket/adapter/JettyWebSocketHandlerAdapter.java | 1 + .../reactive/socket/adapter/JettyWebSocketSession.java | 3 ++- .../adapter/jetty/JettyWebSocketHandlerAdapter.java | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index a8a747a6ddfe..584eddfe1cba 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -47,6 +47,7 @@ * @author Rossen Stoyanchev * @since 5.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { private final WebSocketHandler delegateHandler; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 3db75639f49b..9521b83ede7a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -49,6 +49,7 @@ * @author Rossen Stoyanchev * @since 5.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; @@ -57,7 +58,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private long requested = 0; private boolean awaitingMessage = false; - @SuppressWarnings("NotNullFieldNotInitialized") + @Nullable private FluxSink sink; @Nullable diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index 959ab677f1be..3322fc49f566 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.CloseStatus; @@ -38,14 +39,15 @@ * @author Rossen Stoyanchev * @since 4.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class); private final WebSocketHandler webSocketHandler; private final JettyWebSocketSession wsSession; - @SuppressWarnings("NotNullFieldNotInitialized") - private Session nativeSession; + @Nullable + private Session nativeSession; public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { Assert.notNull(webSocketHandler, "WebSocketHandler must not be null"); @@ -54,7 +56,6 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebS this.wsSession = wsSession; } - @Override public void onWebSocketOpen(Session session) { try { @@ -140,5 +141,4 @@ private void tryCloseWithError(Throwable t) { this.nativeSession.disconnect(); } } - } From 364183cfd895ead6b123d06fc0853dcd165ecd36 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 29 May 2024 16:09:59 +1000 Subject: [PATCH 061/146] fix other checkstyle warnings Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/client/JettyWebSocketClient.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index ae2b185fe4ea..bc770de68147 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.Request; @@ -79,7 +80,7 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); } - AtomicReference handshakeInfo = new AtomicReference<>(); + final AtomicReference handshakeInfo = new AtomicReference<>(); JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { @Override public void onHandshakeResponse(Request request, Response response) { @@ -92,10 +93,10 @@ public void onHandshakeResponse(Request request, Response response) { Sinks.Empty completion = Sinks.empty(); JettyWebSocketHandlerAdapter handlerAdapter = new JettyWebSocketHandlerAdapter(handler, session -> - new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); + new JettyWebSocketSession(session, Objects.requireNonNull(handshakeInfo.get()), DefaultDataBufferFactory.sharedInstance, completion)); try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) - .exceptionally((throwable) -> { + .exceptionally(throwable -> { // Only fail the completion if we have an error // as the JettyWebSocketSession will never be opened. completion.tryEmitError(throwable); From a76465a16abc43be46fb27759f57420788eb3bca Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 29 May 2024 22:56:05 +1000 Subject: [PATCH 062/146] backed out abstract response change upgrade jetty --- framework-platform/framework-platform.gradle | 4 +- .../reactive/JettyCoreServerHttpResponse.java | 179 +++++++++++++----- 2 files changed, 137 insertions(+), 46 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index ac9bfa38369a..193e9bca5778 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -16,8 +16,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.19")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.25.3")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.7")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.7")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.9")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.9")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index be668d03ee50..bb6736b1fe90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,6 +26,9 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -43,6 +46,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -51,6 +55,8 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -59,73 +65,72 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { + private final AtomicBoolean committed = new AtomicBoolean(false); + + private final List>> commitActions = new CopyOnWriteArrayList<>(); private final Response response; - public JettyCoreServerHttpResponse(Response response) { - this(null, response); - } + private final HttpHeaders headers; + + @Nullable + private LinkedMultiValueMap cookies; - public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { - super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, - new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); + public JettyCoreServerHttpResponse(Response response) { this.response = response; + this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + } - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.addCookie(responseCookie); - i.remove(); - } - } + @Override + public HttpHeaders getHeaders() { + return this.headers; } @Override - protected Mono writeWithInternal(Publisher body) { - return Flux.from(body) - .flatMap(this::sendDataBuffer, 1) - .then(); + public DataBufferFactory bufferFactory() { + return DefaultDataBufferFactory.sharedInstance; } @Override - protected Mono writeAndFlushWithInternal(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); + public void beforeCommit(Supplier> action) { + this.commitActions.add(action); } @Override - protected void applyStatusCode() { - HttpStatusCode status = getStatusCode(); - this.response.setStatus(status == null ? 0 : status.value()); + public boolean isCommitted() { + return this.committed.get(); } @Override - protected void applyHeaders() { + public Mono writeWith(Publisher body) { + return Flux.from(body) + .flatMap(this::sendDataBuffer, 1) + .then(); + } + @Override + public Mono writeAndFlushWith(Publisher> body) { + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); } @Override - protected void applyCookies() { - this.getCookies().values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); + public Mono setComplete() { + Mono mono = ensureCommitted(); + return (mono == null) ? Mono.empty() : mono; } @Override public Mono writeWith(Path file, long position, long count) { + Mono mono = ensureCommitted(); + if (mono != null) { + return mono.then(Mono.defer(() -> writeWith(file, position, count))); + } + Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); + mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -135,10 +140,39 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return doCommit(() -> mono); + return mono; + } + + @Nullable + private Mono ensureCommitted() { + if (this.committed.compareAndSet(false, true)) { + if (!this.commitActions.isEmpty()) { + return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) + .concatWith(Mono.fromRunnable(this::writeCookies)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); + } + + writeCookies(); + } + + return null; + } + + private void writeCookies() { + if (this.cookies != null) { + this.cookies.values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); + } } private Mono sendDataBuffer(DataBuffer dataBuffer) { + Mono mono = ensureCommitted(); + if (mono != null) { + return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + } + @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -167,7 +201,41 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return doCommit(() -> Mono.fromFuture(callback)); + return Mono.fromFuture(callback); + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) { + return false; + } + this.response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + int status = this.response.getStatus(); + return HttpStatusCode.valueOf(status == 0 ? 200 : status); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) { + return false; + } + this.response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + return initializeCookies(); + } + + @Override + public void addCookie(ResponseCookie cookie) { + initializeCookies().add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -176,10 +244,33 @@ public T getNativeResponse() { return (T) this.response; } - private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { + private LinkedMultiValueMap initializeCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap<>(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.cookies.add(responseCookie.getName(), responseCookie); + i.remove(); + } + } + } + return this.cookies; + } + + private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public ResponseHttpCookie(ResponseCookie responseCookie) { + public HttpResponseCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } From 82a18eef6e47db93c73b7457a06ae652b7245b23 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 30 May 2024 06:57:42 +1000 Subject: [PATCH 063/146] Steps towards abstract response class --- .../reactive/JettyCoreServerHttpResponse.java | 74 ++++++++----------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bb6736b1fe90..e7eb50da8808 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -74,12 +74,30 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOut private final HttpHeaders headers; - @Nullable - private LinkedMultiValueMap cookies; + private final LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + + private @Nullable HttpStatusCode status; public JettyCoreServerHttpResponse(Response response) { this.response = response; this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.addCookie(responseCookie); + i.remove(); + } + } } @Override @@ -146,6 +164,8 @@ public Mono writeWith(Path file, long position, long count) { @Nullable private Mono ensureCommitted() { if (this.committed.compareAndSet(false, true)) { + if (this.status != null) + this.response.setStatus(this.status.value()); if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -163,7 +183,7 @@ private void writeCookies() { if (this.cookies != null) { this.cookies.values().stream() .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); } } @@ -206,36 +226,27 @@ protected void onCompleteFailure(Throwable cause) { @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) { + if (isCommitted()) { return false; } - this.response.setStatus(status.value()); + this.status = status; return true; } @Override + @Nullable public HttpStatusCode getStatusCode() { - int status = this.response.getStatus(); - return HttpStatusCode.valueOf(status == 0 ? 200 : status); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) { - return false; - } - this.response.setStatus(value); - return true; + return status; } @Override public MultiValueMap getCookies() { - return initializeCookies(); + return this.cookies; } @Override public void addCookie(ResponseCookie cookie) { - initializeCookies().add(cookie.getName(), cookie); + this.cookies.add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -244,33 +255,10 @@ public T getNativeResponse() { return (T) this.response; } - private LinkedMultiValueMap initializeCookies() { - if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(); - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.cookies.add(responseCookie.getName(), responseCookie); - i.remove(); - } - } - } - return this.cookies; - } - - private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public HttpResponseCookie(ResponseCookie responseCookie) { + public ResponseHttpCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } From fca5d22ca4124d85d7075ba378501e885afe77fa Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 30 May 2024 13:46:15 +1000 Subject: [PATCH 064/146] add explicit dependency on jetty-websocket-api Signed-off-by: Lachlan Roberts --- spring-websocket/spring-websocket.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-websocket/spring-websocket.gradle b/spring-websocket/spring-websocket.gradle index 72df03b20dd0..2250f5fdf38b 100644 --- a/spring-websocket/spring-websocket.gradle +++ b/spring-websocket/spring-websocket.gradle @@ -19,6 +19,7 @@ dependencies { optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" From 17eb8df7061a1cd06bd219808af6e6816ba95f1b Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 09:00:11 +1000 Subject: [PATCH 065/146] checkstyle --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index e7eb50da8808..a29d5ff437c9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -164,8 +164,9 @@ public Mono writeWith(Path file, long position, long count) { @Nullable private Mono ensureCommitted() { if (this.committed.compareAndSet(false, true)) { - if (this.status != null) + if (this.status != null) { this.response.setStatus(this.status.value()); + } if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -236,7 +237,7 @@ public boolean setStatusCode(@Nullable HttpStatusCode status) { @Override @Nullable public HttpStatusCode getStatusCode() { - return status; + return this.status; } @Override From 38e49135b234b57709f9e61c081ac2561f055008 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 09:40:57 +1000 Subject: [PATCH 066/146] updated response to use the abstract --- .../reactive/JettyCoreServerHttpResponse.java | 133 ++++-------------- 1 file changed, 26 insertions(+), 107 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index a29d5ff437c9..7e98a67e4cbc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,9 +26,6 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -42,11 +39,11 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -55,8 +52,6 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -65,24 +60,20 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { - private final AtomicBoolean committed = new AtomicBoolean(false); - - private final List>> commitActions = new CopyOnWriteArrayList<>(); +class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { private final Response response; - private final HttpHeaders headers; - - private final LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - - private @Nullable HttpStatusCode status; - public JettyCoreServerHttpResponse(Response response) { + this(null, response); + } + + public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { + super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, + new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); this.response = response; - this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - // remove all existing cookies from the response and add them to the cookie map, to be added back later + // remove all existing cookies from the response and add them to the cookie map, to be added back later for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { HttpField f = i.next(); if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { @@ -101,54 +92,39 @@ public JettyCoreServerHttpResponse(Response response) { } @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public DataBufferFactory bufferFactory() { - return DefaultDataBufferFactory.sharedInstance; + protected Mono writeWithInternal(Publisher body) { + return Flux.from(body) + .flatMap(this::sendDataBuffer, 1) + .then(); } @Override - public void beforeCommit(Supplier> action) { - this.commitActions.add(action); + protected Mono writeAndFlushWithInternal(Publisher> body) { + return Flux.from(body).flatMap(this::writeWithInternal, 1).then(); } @Override - public boolean isCommitted() { - return this.committed.get(); + protected void applyStatusCode() { + HttpStatusCode status = getStatusCode(); + this.response.setStatus(status == null ? 0 : status.value()); } @Override - public Mono writeWith(Publisher body) { - return Flux.from(body) - .flatMap(this::sendDataBuffer, 1) - .then(); - } + protected void applyHeaders() { - @Override - public Mono writeAndFlushWith(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); } @Override - public Mono setComplete() { - Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; + protected void applyCookies() { + this.getCookies().values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); } @Override public Mono writeWith(Path file, long position, long count) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> writeWith(file, position, count))); - } - Callback.Completable callback = new Callback.Completable(); - mono = Mono.fromFuture(callback); + Mono mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -158,42 +134,10 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return mono; - } - - @Nullable - private Mono ensureCommitted() { - if (this.committed.compareAndSet(false, true)) { - if (this.status != null) { - this.response.setStatus(this.status.value()); - } - if (!this.commitActions.isEmpty()) { - return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::writeCookies)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); - } - - writeCookies(); - } - - return null; - } - - private void writeCookies() { - if (this.cookies != null) { - this.cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); - } + return doCommit(() -> mono); } private Mono sendDataBuffer(DataBuffer dataBuffer) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); - } - @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -222,32 +166,7 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return Mono.fromFuture(callback); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted()) { - return false; - } - this.status = status; - return true; - } - - @Override - @Nullable - public HttpStatusCode getStatusCode() { - return this.status; - } - - @Override - public MultiValueMap getCookies() { - return this.cookies; - } - - @Override - public void addCookie(ResponseCookie cookie) { - this.cookies.add(cookie.getName(), cookie); + return doCommit(() -> Mono.fromFuture(callback)); } @SuppressWarnings("unchecked") From 85b404191c117ca0ee861f51ac96de4670e25b69 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 10:06:38 +1000 Subject: [PATCH 067/146] updated response to use the abstract --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 7e98a67e4cbc..4a3332d3e4bf 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -39,7 +39,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; From 37daa27d23d58db8eab5a4a9096190224e3356dd Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 15 Jan 2024 19:05:09 +1100 Subject: [PATCH 068/146] Initial boiler plate implementation of JettyCore HttpHandler Adaptor --- .../reactive/JettyCoreHttpHandlerAdapter.java | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java new file mode 100644 index 000000000000..ccddcd76aed5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -0,0 +1,397 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.util.Callback; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.*; +import org.springframework.http.server.RequestPath; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * + * @author Greg Wilkins + * @since 6.0 + */ +public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { + + private final HttpHandler httpHandler; + + public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { + this.httpHandler = httpHandler; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + httpHandler.handle(new JettyCoreServerHttpRequest(request), new JettyCoreServerHttpResponse(request, response)) + .subscribe(new Subscriber<>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void unused) { + } + + @Override + public void onError(Throwable t) { + callback.failed(t); + } + + @Override + public void onComplete() { + callback.succeeded(); + } + }); + return true; + } + + private static class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable + private MultiValueMap cookies; + + public JettyCoreServerHttpRequest(Request request) { + this.request = request; + headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(request.getMethod()); + } + + @Override + public URI getURI() { + return request.getHttpURI().toURI(); + } + + @Override + public Flux getBody() { + Flow.Publisher flowPublisher = Content.Source.asPublisher(request); + // TODO convert the Flow.Publisher into a org.reactivestreams.Publisher + org.reactivestreams.Publisher publisher = null; + // TODO convert the Publisher to a Flux + Flux chunks = Flux.from(publisher); + // TODO map the chunks to DataBuffers + return chunks.map(chunk -> null); + } + + @Override + public String getId() { + return request.getId(); + } + + @Override + public RequestPath getPath() { + return path; + } + + @Override + public MultiValueMap getQueryParams() { + return null; + } + + @Override + public MultiValueMap getCookies() { + if (cookies == null) { + LinkedHashMap> map = new LinkedHashMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : Request.getCookies(request)) { + List list = map.computeIfAbsent(c.getName(), k -> new ArrayList<>()); + list.add(new HttpCookie(c.getName(), c.getValue())); + } + cookies = new LinkedMultiValueMap<>(map); + } + return cookies; + } + + @Override + public InetSocketAddress getLocalAddress() { + return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public SslInfo getSslInfo() { + if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + return new SslInfo() + { + @Override + public String getSessionId() { + return sslSessionData.sslSessionId(); + } + + @Override + public X509Certificate[] getPeerCertificates() { + return sslSessionData.peerCertificates(); + } + }; + } + return null; + } + + @Override + public Builder mutate() { + return ServerHttpRequest.super.mutate(); + } + } + + private static class JettyCoreServerHttpResponse implements ServerHttpResponse { + enum State { + OPEN, COMMITTED, LAST, COMPLETED + } + private final AtomicReference state = new AtomicReference<>(State.OPEN); + private final Request request; + private final Response response; + private final HttpHeaders headers; + + public JettyCoreServerHttpResponse(Request request, Response response) { + this.request = request; + this.response = response; + headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + request.addHttpStreamWrapper(s -> new HttpStream.Wrapper(s) + { + @Override + public void send(MetaData.Request metaDataRequest, @Nullable MetaData.Response metaDataResponse, boolean last, ByteBuffer content, Callback callback) { + + if (metaDataResponse != null) + request.getContext().run(JettyCoreServerHttpResponse.this::onCommit, request); + if (last) + callback = Callback.from(callback, JettyCoreServerHttpResponse.this::onLast); + + super.send(metaDataRequest, metaDataResponse, last, content, callback); + } + + @Override + public void succeeded() { + super.succeeded(); + onCompleted(null); + } + + @Override + public void failed(Throwable x) { + super.failed(x); + onCompleted(x); + } + }); + } + + private void onCommit() { + // TODO call all the beforeCommit actions + } + + private void onLast() { + + } + + private void onCompleted(@Nullable Throwable failure) { + // TODO trigger any setComplete Monos + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public DataBufferFactory bufferFactory() { + // TODO + return null; + } + + @Override + public void beforeCommit(Supplier> action) { + // TODO + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public Mono writeWith(Publisher body) { + // TODO + return null; + } + + @Override + public Mono writeAndFlushWith(Publisher> body) { + // TODO + return null; + } + + @Override + public Mono setComplete() { + // TODO + return null; + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) + return false; + response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + return HttpStatusCode.valueOf(response.getStatus()); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) + return false; + response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } + return cookies; + } + + @Override + public void addCookie(ResponseCookie cookie) { + Response.addCookie(response, new HttpResponseCookie(cookie)); + } + + private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private final ResponseCookie responseCookie; + + public HttpResponseCookie(ResponseCookie responseCookie) { + this.responseCookie = responseCookie; + } + + public ResponseCookie getResponseCookie() { + return responseCookie; + } + + @Override + public String getName() { + return responseCookie.getName(); + } + + @Override + public String getValue() { + return responseCookie.getValue(); + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public long getMaxAge() { + return responseCookie.getMaxAge().toSeconds(); + } + + @Override + @Nullable + public String getComment() { + return null; + } + + @Override + @Nullable + public String getDomain() { + return responseCookie.getDomain(); + } + + @Override + @Nullable + public String getPath() { + return responseCookie.getPath(); + } + + @Override + public boolean isSecure() { + return responseCookie.isSecure(); + } + + @Override + public SameSite getSameSite() { + String sameSiteName = responseCookie.getSameSite(); + if (sameSiteName != null) + return SameSite.valueOf(sameSiteName); + SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); + return sameSite == null ? SameSite.NONE : sameSite; + } + + @Override + public boolean isHttpOnly() { + return responseCookie.isHttpOnly(); + } + + @Override + public boolean isPartitioned() { + return false; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + } + } +} From c5b37ba79fe151b4c0b74f6fed0406331057b539 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:29:21 +1100 Subject: [PATCH 069/146] More implementation of the request side --- .../reactive/JettyCoreHttpHandlerAdapter.java | 76 ++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index ccddcd76aed5..b32c18aed105 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -21,16 +21,20 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.*; -import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.*; +import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.*; import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import reactor.core.publisher.Flux; @@ -41,21 +45,35 @@ import java.nio.ByteBuffer; import java.security.cert.X509Certificate; import java.util.*; -import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; /** + * + * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. * * @author Greg Wilkins - * @since 6.0 + * @author Lachlan Roberts + * @since 6.1.4 */ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { private final HttpHandler httpHandler; + private final DataBufferFactory dataBufferFactory; public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; + + // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use + // wrap and there should never be any allocation done by the factory. Also there is no release semantic + // available. + dataBufferFactory = new DefaultDataBufferFactory() + { + @Override + public DefaultDataBuffer allocateBuffer(int initialCapacity) { + throw new UnsupportedOperationException(); + } + }; } @Override @@ -69,6 +87,7 @@ public void onSubscribe(Subscription s) { @Override public void onNext(Void unused) { + // we can ignore the void as we only seek onError or onComplete } @Override @@ -84,11 +103,17 @@ public void onComplete() { return true; } - private static class JettyCoreServerHttpRequest implements ServerHttpRequest { + private class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private final Request request; private final HttpHeaders headers; private final RequestPath path; @Nullable + private URI uri; + @Nullable + MultiValueMap queryParameters; + @Nullable private MultiValueMap cookies; public JettyCoreServerHttpRequest(Request request) { @@ -109,18 +134,18 @@ public HttpMethod getMethod() { @Override public URI getURI() { - return request.getHttpURI().toURI(); + if (uri == null) + uri = request.getHttpURI().toURI(); + return uri; } @Override public Flux getBody() { - Flow.Publisher flowPublisher = Content.Source.asPublisher(request); - // TODO convert the Flow.Publisher into a org.reactivestreams.Publisher - org.reactivestreams.Publisher publisher = null; - // TODO convert the Publisher to a Flux - Flux chunks = Flux.from(publisher); - // TODO map the chunks to DataBuffers - return chunks.map(chunk -> null); + // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and + // then wrapped as a Flux. The chunks are converted to DataBuffers with simple wrapping and will be released + // by the Flow.Publisher on return from onNext, so that any retention of the data must be done by a copy within + // the call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(chunk -> dataBufferFactory.wrap(chunk.getByteBuffer())); } @Override @@ -135,18 +160,33 @@ public RequestPath getPath() { @Override public MultiValueMap getQueryParams() { - return null; + if (queryParameters == null) + { + String query = request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) + queryParameters = EMPTY_QUERY; + else { + MultiMap map = new MultiMap<>(); + UrlEncoded.decodeUtf8To(query, 0, query.length(), map); + queryParameters = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(map)); + } + } + return queryParameters; } @Override public MultiValueMap getCookies() { if (cookies == null) { - LinkedHashMap> map = new LinkedHashMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : Request.getCookies(request)) { - List list = map.computeIfAbsent(c.getName(), k -> new ArrayList<>()); - list.add(new HttpCookie(c.getName(), c.getValue())); + List httpCookies = Request.getCookies(request); + if (httpCookies.isEmpty()) + cookies = EMPTY_COOKIES; + else { + cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); } - cookies = new LinkedMultiValueMap<>(map); } return cookies; } From 0ea2180f72af3173d43638df6cb17bbd86ac3ab7 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:56:02 +1100 Subject: [PATCH 070/146] Created JettyCoreHttpServer --- .../AbstractHttpHandlerIntegrationTests.java | 1 + .../bootstrap/JettyCoreHttpServer.java | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java index f49b91fec5f6..0aa39aa025e6 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/AbstractHttpHandlerIntegrationTests.java @@ -126,6 +126,7 @@ public static Flux testInterval(Duration period, int count) { static Stream> httpServers() { return Stream.of( named("Jetty", new JettyHttpServer()), + named("Jetty Core", new JettyCoreHttpServer()), named("Reactor Netty", new ReactorHttpServer()), named("Tomcat", new TomcatHttpServer()), named("Undertow", new UndertowHttpServer()) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java new file mode 100644 index 000000000000..637873d24bf1 --- /dev/null +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.testfixture.http.server.reactive.bootstrap; + +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; + +/** + * @author Rossen Stoyanchev + * @author Sam Brannen + * @author Greg Wilkins + */ +public class JettyCoreHttpServer extends AbstractHttpServer { + + private Server jettyServer; + + + @Override + protected void initServer() { + this.jettyServer = new Server(); + + ServerConnector connector = new ServerConnector(this.jettyServer); + connector.setHost(getHost()); + connector.setPort(getPort()); + this.jettyServer.addConnector(connector); + this.jettyServer.setHandler(createHandlerAdapter()); + } + + private JettyCoreHttpHandlerAdapter createHandlerAdapter() { + return new JettyCoreHttpHandlerAdapter(resolveHttpHandler()); + } + + @Override + protected void startInternal() throws Exception { + this.jettyServer.start(); + setPort(((ServerConnector) this.jettyServer.getConnectors()[0]).getLocalPort()); + } + + @Override + protected void stopInternal() { + try { + if (this.jettyServer.isRunning()) { + // Do not configure a large stop timeout. For example, setting a stop timeout + // of 5000 adds an additional 1-2 seconds to the runtime of each test using + // the Jetty sever, resulting in 2-4 extra minutes of overall build time. + this.jettyServer.setStopTimeout(100); + this.jettyServer.stop(); + this.jettyServer.destroy(); + } + } + catch (Exception ex) { + // ignore + } + } + + @Override + protected void resetInternal() { + try { + stopInternal(); + } + finally { + this.jettyServer = null; + } + } + +} From 49363c1ea3b2dac2836d2912d55d943e80922da8 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 10:59:37 +1100 Subject: [PATCH 071/146] Created JettyCoreHttpServer --- .../reactive/ErrorHandlerIntegrationTests.java | 5 +++-- .../method/annotation/SseIntegrationTests.java | 12 +++++------- .../AbstractReactiveWebSocketIntegrationTests.java | 7 ++----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java index eeb1bda99a20..d29e711381d2 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -18,6 +18,7 @@ import java.net.URI; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; @@ -87,8 +88,8 @@ void emptyPathSegments(HttpServer httpServer) throws Exception { // but an application can apply CompactPathRule via RewriteHandler: // https://www.eclipse.org/jetty/documentation/jetty-11/programming_guide.php - HttpStatus expectedStatus = - (httpServer instanceof JettyHttpServer ? HttpStatus.BAD_REQUEST : HttpStatus.OK); + HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer + ? HttpStatus.BAD_REQUEST : HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(expectedStatus); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index 6ffd05022e82..a3dbe1a10ca8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -51,12 +52,6 @@ import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -127,7 +122,7 @@ void sseAsPerson(HttpServer httpServer, ClientHttpConnector connector) throws Ex @ParameterizedSseTest void sseAsEvent(HttpServer httpServer, ClientHttpConnector connector) throws Exception { - assumeTrue(httpServer instanceof JettyHttpServer); + assumeTrue(httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer); startServer(httpServer, connector); @@ -305,6 +300,9 @@ static Stream arguments() { args(new JettyHttpServer(), new ReactorClientHttpConnector()), args(new JettyHttpServer(), new JettyClientHttpConnector()), args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), + args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), + args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), + args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), args(new ReactorHttpServer(), new ReactorClientHttpConnector()), args(new ReactorHttpServer(), new JettyClientHttpConnector()), args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index d6954505fa1e..fe5909ca8776 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import org.xnio.OptionMap; import org.xnio.Xnio; import reactor.core.publisher.Flux; @@ -60,11 +61,6 @@ import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; /** * Base class for reactive WebSocket integration tests. Subclasses must implement @@ -97,6 +93,7 @@ static Stream arguments() throws IOException { Map> servers = new LinkedHashMap<>(); servers.put(new TomcatHttpServer(TMP_DIR.getAbsolutePath(), WsContextListener.class), TomcatConfig.class); servers.put(new JettyHttpServer(), JettyConfig.class); + servers.put(new JettyCoreHttpServer(), JettyConfig.class); servers.put(new ReactorHttpServer(), ReactorNettyConfig.class); servers.put(new UndertowHttpServer(), UndertowConfig.class); From 090c8c4c3df15544865d426e1ad79dde06859654 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:16:50 +1100 Subject: [PATCH 072/146] WIP on tests --- .../reactive/AbstractServerHttpRequest.java | 2 +- .../reactive/JettyCoreHttpHandlerAdapter.java | 24 ++++++++++++++----- .../bootstrap/JettyCoreHttpServer.java | 1 + 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 829a2202a814..da4659cd3e61 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -42,7 +42,7 @@ */ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { - private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); + static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); private final URI uri; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index b32c18aed105..c4d0ebf1ed0b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -37,16 +37,22 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.InetSocketAddress; import java.net.URI; +import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import java.util.regex.Matcher; + +import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** * @@ -166,9 +172,15 @@ public MultiValueMap getQueryParams() { if (StringUtil.isBlank(query)) queryParameters = EMPTY_QUERY; else { - MultiMap map = new MultiMap<>(); - UrlEncoded.decodeUtf8To(query, 0, query.length(), map); - queryParameters = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(map)); + queryParameters = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + queryParameters.add(name, value); + } } } return queryParameters; @@ -304,19 +316,19 @@ public boolean isCommitted() { @Override public Mono writeWith(Publisher body) { // TODO - return null; + return Mono.empty(); } @Override public Mono writeAndFlushWith(Publisher> body) { // TODO - return null; + return Mono.empty(); } @Override public Mono setComplete() { // TODO - return null; + return Mono.empty(); } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 637873d24bf1..14d9821a00b4 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -43,6 +43,7 @@ protected void initServer() { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(createHandlerAdapter()); + // TODO add websocket upgrade handler } private JettyCoreHttpHandlerAdapter createHandlerAdapter() { From 390f52eea6db36753686cf3169abf33bd3e4c39a Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:24:45 +1100 Subject: [PATCH 073/146] WIP on tests --- .../server/reactive/JettyCoreHttpHandlerAdapter.java | 12 +++--------- .../server/reactive/ZeroCopyIntegrationTests.java | 7 ++----- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index c4d0ebf1ed0b..ac8c0395b375 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -71,15 +71,9 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use - // wrap and there should never be any allocation done by the factory. Also there is no release semantic - // available. - dataBufferFactory = new DefaultDataBufferFactory() - { - @Override - public DefaultDataBuffer allocateBuffer(int initialCapacity) { - throw new UnsupportedOperationException(); - } - }; + // wrap and there should rarely be any allocation done by the factory. Also, there is no release semantic + // available so we could not do retainable buffers anyway. + dataBufferFactory = new DefaultDataBufferFactory(); } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index 587825b0b066..c4f24f69d18d 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -19,6 +19,7 @@ import java.io.File; import java.net.URI; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Mono; import org.springframework.core.io.ClassPathResource; @@ -28,10 +29,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.web.client.RestTemplate; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -54,7 +51,7 @@ protected HttpHandler createHttpHandler() { @ParameterizedHttpServerTest void zeroCopy(HttpServer httpServer) throws Exception { - assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer, + assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer || httpServer instanceof JettyCoreHttpServer, "Zero-copy does not support Servlet"); startServer(httpServer); From b884f65f75a40f7a45c49a948a608cd2a949507f Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 15:42:21 +1100 Subject: [PATCH 074/146] Cleanup start/stop for jetty --- .../reactive/JettyCoreHttpHandlerAdapter.java | 39 +------------------ .../bootstrap/JettyCoreHttpServer.java | 11 +----- .../reactive/bootstrap/JettyHttpServer.java | 31 +++------------ 3 files changed, 8 insertions(+), 73 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index ac8c0395b375..9377468ff0ed 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -247,43 +247,6 @@ public JettyCoreServerHttpResponse(Request request, Response response) { this.request = request; this.response = response; headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - request.addHttpStreamWrapper(s -> new HttpStream.Wrapper(s) - { - @Override - public void send(MetaData.Request metaDataRequest, @Nullable MetaData.Response metaDataResponse, boolean last, ByteBuffer content, Callback callback) { - - if (metaDataResponse != null) - request.getContext().run(JettyCoreServerHttpResponse.this::onCommit, request); - if (last) - callback = Callback.from(callback, JettyCoreServerHttpResponse.this::onLast); - - super.send(metaDataRequest, metaDataResponse, last, content, callback); - } - - @Override - public void succeeded() { - super.succeeded(); - onCompleted(null); - } - - @Override - public void failed(Throwable x) { - super.failed(x); - onCompleted(x); - } - }); - } - - private void onCommit() { - // TODO call all the beforeCommit actions - } - - private void onLast() { - - } - - private void onCompleted(@Nullable Throwable failure) { - // TODO trigger any setComplete Monos } @Override @@ -299,7 +262,7 @@ public DataBufferFactory bufferFactory() { @Override public void beforeCommit(Supplier> action) { - // TODO + // TODO See UndertowServerHttpResponse as an example } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 14d9821a00b4..eb5aad83fd1f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -59,14 +59,7 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() { try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } + this.jettyServer.stop(); } catch (Exception ex) { // ignore @@ -77,10 +70,10 @@ protected void stopInternal() { protected void resetInternal() { try { stopInternal(); + this.jettyServer.destroy(); } finally { this.jettyServer = null; } } - } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java index d3912ac2df8c..08b7d32b3962 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java @@ -54,7 +54,6 @@ protected void initServer() throws Exception { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(this.contextHandler); - this.contextHandler.start(); } private ServletHttpHandlerAdapter createServletAdapter() { @@ -70,43 +69,23 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() throws Exception { try { - if (this.contextHandler.isRunning()) { - this.contextHandler.stop(); - } + this.jettyServer.stop(); } - finally { - try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } - } - catch (Exception ex) { - // ignore - } + catch (Exception ex) { + // ignore } } @Override protected void resetInternal() { try { - if (this.jettyServer.isRunning()) { - // Do not configure a large stop timeout. For example, setting a stop timeout - // of 5000 adds an additional 1-2 seconds to the runtime of each test using - // the Jetty sever, resulting in 2-4 extra minutes of overall build time. - this.jettyServer.setStopTimeout(100); - this.jettyServer.stop(); - this.jettyServer.destroy(); - } + this.jettyServer.stop(); } catch (Exception ex) { throw new IllegalStateException(ex); } finally { + this.jettyServer.destroy(); this.jettyServer = null; this.contextHandler = null; } From 2aedef1b339ad9b9401a3163c0bb4c33e7400715 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 16:57:48 +1100 Subject: [PATCH 075/146] retainable bytebuffer! --- .../reactive/JettyCoreHttpHandlerAdapter.java | 270 +++++++++++++++++- 1 file changed, 258 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 9377468ff0ed..54874b2054d9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -17,19 +17,16 @@ package org.springframework.http.server.reactive; import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.Retainable; import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.*; import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBuffer; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.*; import org.springframework.http.*; import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; @@ -41,14 +38,19 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntPredicate; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -70,9 +72,10 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; - // We do not make a DataBufferFactory over the servers ByteBufferPool, because we only ever use - // wrap and there should rarely be any allocation done by the factory. Also, there is no release semantic - // available so we could not do retainable buffers anyway. + // TODO currently we do not make a DataBufferFactory over the servers ByteBufferPool, + // because we mainly use wrap and there should be few allocation done by the factory. + // But it should be possible to use the servers buffer pool for allocations and to + // create PooledDataBuffers dataBufferFactory = new DefaultDataBufferFactory(); } @@ -142,10 +145,9 @@ public URI getURI() { @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to DataBuffers with simple wrapping and will be released - // by the Flow.Publisher on return from onNext, so that any retention of the data must be done by a copy within - // the call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(chunk -> dataBufferFactory.wrap(chunk.getByteBuffer())); + // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be + // retained within a call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(RetainedDataBuffer::new); } @Override @@ -403,4 +405,248 @@ public Map getAttributes() { } } } + + class RetainedDataBuffer implements PooledDataBuffer + { + private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); + + public RetainedDataBuffer(Content.Chunk chunk) { + this(chunk.getByteBuffer(), chunk); + } + + public RetainedDataBuffer(ByteBuffer byteBuffer, Retainable retainable) { + this.dataBuffer = dataBufferFactory.wrap(byteBuffer); + this.retainable = retainable; + } + + @Override + public boolean isAllocated() { + return allocated.get(); + } + + @Override + public PooledDataBuffer retain() { + retainable.retain(); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + return this; + } + + @Override + public boolean release() { + if (retainable.release()) { + allocated.set(false); + return true; + } + return false; + } + + @Override + public DataBufferFactory factory() { + return dataBuffer.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return dataBuffer.readableByteCount(); + } + + @Override + public int writableByteCount() { + return dataBuffer.writableByteCount(); + } + + @Override + public int capacity() { + return dataBuffer.capacity(); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer capacity(int capacity) { + return dataBuffer.capacity(capacity); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer ensureCapacity(int capacity) { + return dataBuffer.ensureCapacity(capacity); + } + + @Override + public DataBuffer ensureWritable(int capacity) { + return dataBuffer.ensureWritable(capacity); + } + + @Override + public int readPosition() { + return dataBuffer.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + return dataBuffer.readPosition(readPosition); + } + + @Override + public int writePosition() { + return dataBuffer.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + return dataBuffer.writePosition(writePosition); + } + + @Override + public byte getByte(int index) { + return dataBuffer.getByte(index); + } + + @Override + public byte read() { + return dataBuffer.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return dataBuffer.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return dataBuffer.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return dataBuffer.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return dataBuffer.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return dataBuffer.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + return dataBuffer.write(charSequence, charset); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer slice(int index, int length) { + return dataBuffer.slice(index, length); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer retainedSlice(int index, int length) { + return dataBuffer.retainedSlice(index, length); + } + + @Override + public DataBuffer split(int index) { + return dataBuffer.split(index); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer() { + return dataBuffer.asByteBuffer(); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer(int index, int length) { + return dataBuffer.asByteBuffer(index, length); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer() { + return dataBuffer.toByteBuffer(); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer(int index, int length) { + return dataBuffer.toByteBuffer(index, length); + } + + @Override + public void toByteBuffer(ByteBuffer dest) { + dataBuffer.toByteBuffer(dest); + } + + @Override + public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + } + + @Override + public ByteBufferIterator readableByteBuffers() { + return dataBuffer.readableByteBuffers(); + } + + @Override + public ByteBufferIterator writableByteBuffers() { + return dataBuffer.writableByteBuffers(); + } + + @Override + public InputStream asInputStream() { + return dataBuffer.asInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return dataBuffer.asInputStream(releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return dataBuffer.asOutputStream(); + } + + @Override + public String toString(Charset charset) { + return dataBuffer.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + return dataBuffer.toString(index, length, charset); + } + } } From 4613e8c228155042e317955ed4cc050e5d21bf06 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 17:04:34 +1100 Subject: [PATCH 076/146] Non blocking invocation type --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 54874b2054d9..6ea806bca188 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -57,14 +57,13 @@ import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** - * * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. * * @author Greg Wilkins * @author Lachlan Roberts * @since 6.1.4 */ -public class JettyCoreHttpHandlerAdapter extends Handler.Abstract { +public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; private final DataBufferFactory dataBufferFactory; From 5378423b2e7175e05048aa084a40bc346c9d99a0 Mon Sep 17 00:00:00 2001 From: gregw Date: Tue, 16 Jan 2024 17:18:34 +1100 Subject: [PATCH 077/146] split out into separate files --- .../reactive/JettyCoreHttpHandlerAdapter.java | 579 +----------------- .../reactive/JettyCoreServerHttpRequest.java | 187 ++++++ .../reactive/JettyCoreServerHttpResponse.java | 216 +++++++ .../reactive/JettyRetainedDataBuffer.java | 276 +++++++++ 4 files changed, 680 insertions(+), 578 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 6ea806bca188..f861606b60ec 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -16,45 +16,11 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.Retainable; import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.*; -import org.reactivestreams.FlowAdapters; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.*; -import org.springframework.http.*; -import org.springframework.http.server.RequestPath; -import org.springframework.http.support.JettyHeadersAdapter; -import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.IntPredicate; -import java.util.function.Supplier; -import java.util.regex.Matcher; - -import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. @@ -80,7 +46,7 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(request), new JettyCoreServerHttpResponse(request, response)) + httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(request, response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { @@ -105,547 +71,4 @@ public void onComplete() { return true; } - private class JettyCoreServerHttpRequest implements ServerHttpRequest { - private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final Request request; - private final HttpHeaders headers; - private final RequestPath path; - @Nullable - private URI uri; - @Nullable - MultiValueMap queryParameters; - @Nullable - private MultiValueMap cookies; - - public JettyCoreServerHttpRequest(Request request) { - this.request = request; - headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(request.getMethod()); - } - - @Override - public URI getURI() { - if (uri == null) - uri = request.getHttpURI().toURI(); - return uri; - } - - @Override - public Flux getBody() { - // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be - // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(RetainedDataBuffer::new); - } - - @Override - public String getId() { - return request.getId(); - } - - @Override - public RequestPath getPath() { - return path; - } - - @Override - public MultiValueMap getQueryParams() { - if (queryParameters == null) - { - String query = request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) - queryParameters = EMPTY_QUERY; - else { - queryParameters = new LinkedMultiValueMap<>(); - Matcher matcher = QUERY_PATTERN.matcher(query); - while (matcher.find()) { - String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - String eq = matcher.group(2); - String value = matcher.group(3); - value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - queryParameters.add(name, value); - } - } - } - return queryParameters; - } - - @Override - public MultiValueMap getCookies() { - if (cookies == null) { - List httpCookies = Request.getCookies(request); - if (httpCookies.isEmpty()) - cookies = EMPTY_COOKIES; - else { - cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { - cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } - cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); - } - } - return cookies; - } - - @Override - public InetSocketAddress getLocalAddress() { - return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet - ? inet : null; - } - - @Override - public InetSocketAddress getRemoteAddress() { - return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet - ? inet : null; - } - - @Override - public SslInfo getSslInfo() { - if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { - return new SslInfo() - { - @Override - public String getSessionId() { - return sslSessionData.sslSessionId(); - } - - @Override - public X509Certificate[] getPeerCertificates() { - return sslSessionData.peerCertificates(); - } - }; - } - return null; - } - - @Override - public Builder mutate() { - return ServerHttpRequest.super.mutate(); - } - } - - private static class JettyCoreServerHttpResponse implements ServerHttpResponse { - enum State { - OPEN, COMMITTED, LAST, COMPLETED - } - private final AtomicReference state = new AtomicReference<>(State.OPEN); - private final Request request; - private final Response response; - private final HttpHeaders headers; - - public JettyCoreServerHttpResponse(Request request, Response response) { - this.request = request; - this.response = response; - headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public DataBufferFactory bufferFactory() { - // TODO - return null; - } - - @Override - public void beforeCommit(Supplier> action) { - // TODO See UndertowServerHttpResponse as an example - } - - @Override - public boolean isCommitted() { - return response.isCommitted(); - } - - @Override - public Mono writeWith(Publisher body) { - // TODO - return Mono.empty(); - } - - @Override - public Mono writeAndFlushWith(Publisher> body) { - // TODO - return Mono.empty(); - } - - @Override - public Mono setComplete() { - // TODO - return Mono.empty(); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) - return false; - response.setStatus(status.value()); - return true; - } - - @Override - public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(response.getStatus()); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) - return false; - response.setStatus(value); - return true; - } - - @Override - public MultiValueMap getCookies() { - LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); - } - return cookies; - } - - @Override - public void addCookie(ResponseCookie cookie) { - Response.addCookie(response, new HttpResponseCookie(cookie)); - } - - private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { - private final ResponseCookie responseCookie; - - public HttpResponseCookie(ResponseCookie responseCookie) { - this.responseCookie = responseCookie; - } - - public ResponseCookie getResponseCookie() { - return responseCookie; - } - - @Override - public String getName() { - return responseCookie.getName(); - } - - @Override - public String getValue() { - return responseCookie.getValue(); - } - - @Override - public int getVersion() { - return 0; - } - - @Override - public long getMaxAge() { - return responseCookie.getMaxAge().toSeconds(); - } - - @Override - @Nullable - public String getComment() { - return null; - } - - @Override - @Nullable - public String getDomain() { - return responseCookie.getDomain(); - } - - @Override - @Nullable - public String getPath() { - return responseCookie.getPath(); - } - - @Override - public boolean isSecure() { - return responseCookie.isSecure(); - } - - @Override - public SameSite getSameSite() { - String sameSiteName = responseCookie.getSameSite(); - if (sameSiteName != null) - return SameSite.valueOf(sameSiteName); - SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); - return sameSite == null ? SameSite.NONE : sameSite; - } - - @Override - public boolean isHttpOnly() { - return responseCookie.isHttpOnly(); - } - - @Override - public boolean isPartitioned() { - return false; - } - - @Override - public Map getAttributes() { - return Collections.emptyMap(); - } - } - } - - class RetainedDataBuffer implements PooledDataBuffer - { - private final Retainable retainable; - private final DataBuffer dataBuffer; - private final AtomicBoolean allocated = new AtomicBoolean(true); - - public RetainedDataBuffer(Content.Chunk chunk) { - this(chunk.getByteBuffer(), chunk); - } - - public RetainedDataBuffer(ByteBuffer byteBuffer, Retainable retainable) { - this.dataBuffer = dataBufferFactory.wrap(byteBuffer); - this.retainable = retainable; - } - - @Override - public boolean isAllocated() { - return allocated.get(); - } - - @Override - public PooledDataBuffer retain() { - retainable.retain(); - return this; - } - - @Override - public PooledDataBuffer touch(Object hint) { - return this; - } - - @Override - public boolean release() { - if (retainable.release()) { - allocated.set(false); - return true; - } - return false; - } - - @Override - public DataBufferFactory factory() { - return dataBuffer.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return dataBuffer.readableByteCount(); - } - - @Override - public int writableByteCount() { - return dataBuffer.writableByteCount(); - } - - @Override - public int capacity() { - return dataBuffer.capacity(); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer capacity(int capacity) { - return dataBuffer.capacity(capacity); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer ensureCapacity(int capacity) { - return dataBuffer.ensureCapacity(capacity); - } - - @Override - public DataBuffer ensureWritable(int capacity) { - return dataBuffer.ensureWritable(capacity); - } - - @Override - public int readPosition() { - return dataBuffer.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - return dataBuffer.readPosition(readPosition); - } - - @Override - public int writePosition() { - return dataBuffer.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - return dataBuffer.writePosition(writePosition); - } - - @Override - public byte getByte(int index) { - return dataBuffer.getByte(index); - } - - @Override - public byte read() { - return dataBuffer.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - return dataBuffer.read(destination); - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - return dataBuffer.read(destination, offset, length); - } - - @Override - public DataBuffer write(byte b) { - return dataBuffer.write(b); - } - - @Override - public DataBuffer write(byte[] source) { - return dataBuffer.write(source); - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - return dataBuffer.write(source, offset, length); - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - return dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - return dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - return dataBuffer.write(charSequence, charset); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer slice(int index, int length) { - return dataBuffer.slice(index, length); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer retainedSlice(int index, int length) { - return dataBuffer.retainedSlice(index, length); - } - - @Override - public DataBuffer split(int index) { - return dataBuffer.split(index); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer() { - return dataBuffer.asByteBuffer(); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer(int index, int length) { - return dataBuffer.asByteBuffer(index, length); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer() { - return dataBuffer.toByteBuffer(); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer(int index, int length) { - return dataBuffer.toByteBuffer(index, length); - } - - @Override - public void toByteBuffer(ByteBuffer dest) { - dataBuffer.toByteBuffer(dest); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dataBuffer.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return dataBuffer.readableByteBuffers(); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return dataBuffer.writableByteBuffers(); - } - - @Override - public InputStream asInputStream() { - return dataBuffer.asInputStream(); - } - - @Override - public InputStream asInputStream(boolean releaseOnClose) { - return dataBuffer.asInputStream(releaseOnClose); - } - - @Override - public OutputStream asOutputStream() { - return dataBuffer.asOutputStream(); - } - - @Override - public String toString(Charset charset) { - return dataBuffer.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - return dataBuffer.toString(index, length, charset); - } - } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java new file mode 100644 index 000000000000..9e5c45bc49dc --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; +import org.reactivestreams.FlowAdapters; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.RequestPath; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.regex.Matcher; + +import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; + +/** + * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +class JettyCoreServerHttpRequest implements ServerHttpRequest { + private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final DataBufferFactory dataBufferFactory; + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable + private URI uri; + @Nullable + MultiValueMap queryParameters; + @Nullable + private MultiValueMap cookies; + + public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { + this.dataBufferFactory = dataBufferFactory; + this.request = request; + headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(request.getMethod()); + } + + @Override + public URI getURI() { + if (uri == null) + uri = request.getHttpURI().toURI(); + return uri; + } + + @Override + public Flux getBody() { + // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and + // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be + // retained within a call to onNext. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(this::wrap); + } + + private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { + return new JettyRetainedDataBuffer(dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + } + + @Override + public String getId() { + return request.getId(); + } + + @Override + public RequestPath getPath() { + return path; + } + + @Override + public MultiValueMap getQueryParams() { + if (queryParameters == null) { + String query = request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) + queryParameters = EMPTY_QUERY; + else { + queryParameters = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + queryParameters.add(name, value); + } + } + } + return queryParameters; + } + + @Override + public MultiValueMap getCookies() { + if (cookies == null) { + List httpCookies = Request.getCookies(request); + if (httpCookies.isEmpty()) + cookies = EMPTY_COOKIES; + else { + cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); + } + } + return cookies; + } + + @Override + public InetSocketAddress getLocalAddress() { + return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + ? inet : null; + } + + @Override + public SslInfo getSslInfo() { + if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + return new SslInfo() { + @Override + public String getSessionId() { + return sslSessionData.sslSessionId(); + } + + @Override + public X509Certificate[] getPeerCertificates() { + return sslSessionData.peerCertificates(); + } + }; + } + return null; + } + + @Override + public Builder mutate() { + return ServerHttpRequest.super.mutate(); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java new file mode 100644 index 000000000000..89342e79d433 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.server.HttpCookieUtils; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.reactivestreams.Publisher; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseCookie; +import org.springframework.http.support.JettyHeadersAdapter; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +class JettyCoreServerHttpResponse implements ServerHttpResponse { + enum State { + OPEN, COMMITTED, LAST, COMPLETED + } + + private final AtomicReference state = new AtomicReference<>(State.OPEN); + private final Request request; + private final Response response; + private final HttpHeaders headers; + + public JettyCoreServerHttpResponse(Request request, Response response) { + this.request = request; + this.response = response; + headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public DataBufferFactory bufferFactory() { + // TODO + return null; + } + + @Override + public void beforeCommit(Supplier> action) { + // TODO See UndertowServerHttpResponse as an example + } + + @Override + public boolean isCommitted() { + return response.isCommitted(); + } + + @Override + public Mono writeWith(Publisher body) { + // TODO + return Mono.empty(); + } + + @Override + public Mono writeAndFlushWith(Publisher> body) { + // TODO + return Mono.empty(); + } + + @Override + public Mono setComplete() { + // TODO + return Mono.empty(); + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) + return false; + response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + return HttpStatusCode.valueOf(response.getStatus()); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) + return false; + response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } + return cookies; + } + + @Override + public void addCookie(ResponseCookie cookie) { + Response.addCookie(response, new HttpResponseCookie(cookie)); + } + + private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private final ResponseCookie responseCookie; + + public HttpResponseCookie(ResponseCookie responseCookie) { + this.responseCookie = responseCookie; + } + + public ResponseCookie getResponseCookie() { + return responseCookie; + } + + @Override + public String getName() { + return responseCookie.getName(); + } + + @Override + public String getValue() { + return responseCookie.getValue(); + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public long getMaxAge() { + return responseCookie.getMaxAge().toSeconds(); + } + + @Override + @Nullable + public String getComment() { + return null; + } + + @Override + @Nullable + public String getDomain() { + return responseCookie.getDomain(); + } + + @Override + @Nullable + public String getPath() { + return responseCookie.getPath(); + } + + @Override + public boolean isSecure() { + return responseCookie.isSecure(); + } + + @Override + public SameSite getSameSite() { + String sameSiteName = responseCookie.getSameSite(); + if (sameSiteName != null) + return SameSite.valueOf(sameSiteName); + SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); + return sameSite == null ? SameSite.NONE : sameSite; + } + + @Override + public boolean isHttpOnly() { + return responseCookie.isHttpOnly(); + } + + @Override + public boolean isPartitioned() { + return false; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java new file mode 100644 index 000000000000..5b9c2fbd1bd7 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.server.reactive; + +import org.eclipse.jetty.io.Retainable; +import org.eclipse.jetty.server.Response; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntPredicate; + +/** + * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @since 6.1.4 + */ +public class JettyRetainedDataBuffer implements PooledDataBuffer { + private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); + + public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { + this.dataBuffer = dataBuffer; + this.retainable = retainable; + } + + @Override + public boolean isAllocated() { + return allocated.get(); + } + + @Override + public PooledDataBuffer retain() { + retainable.retain(); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + return this; + } + + @Override + public boolean release() { + if (retainable.release()) { + allocated.set(false); + return true; + } + return false; + } + + @Override + public DataBufferFactory factory() { + return dataBuffer.factory(); + } + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return dataBuffer.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return dataBuffer.readableByteCount(); + } + + @Override + public int writableByteCount() { + return dataBuffer.writableByteCount(); + } + + @Override + public int capacity() { + return dataBuffer.capacity(); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer capacity(int capacity) { + return dataBuffer.capacity(capacity); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer ensureCapacity(int capacity) { + return dataBuffer.ensureCapacity(capacity); + } + + @Override + public DataBuffer ensureWritable(int capacity) { + return dataBuffer.ensureWritable(capacity); + } + + @Override + public int readPosition() { + return dataBuffer.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + return dataBuffer.readPosition(readPosition); + } + + @Override + public int writePosition() { + return dataBuffer.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + return dataBuffer.writePosition(writePosition); + } + + @Override + public byte getByte(int index) { + return dataBuffer.getByte(index); + } + + @Override + public byte read() { + return dataBuffer.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + return dataBuffer.read(destination); + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + return dataBuffer.read(destination, offset, length); + } + + @Override + public DataBuffer write(byte b) { + return dataBuffer.write(b); + } + + @Override + public DataBuffer write(byte[] source) { + return dataBuffer.write(source); + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + return dataBuffer.write(source, offset, length); + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + return dataBuffer.write(buffers); + } + + @Override + public DataBuffer write(CharSequence charSequence, Charset charset) { + return dataBuffer.write(charSequence, charset); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer slice(int index, int length) { + return dataBuffer.slice(index, length); + } + + @Override + @Deprecated(since = "6.0") + public DataBuffer retainedSlice(int index, int length) { + return dataBuffer.retainedSlice(index, length); + } + + @Override + public DataBuffer split(int index) { + return dataBuffer.split(index); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer() { + return dataBuffer.asByteBuffer(); + } + + @Override + @Deprecated(since = "6.0") + public ByteBuffer asByteBuffer(int index, int length) { + return dataBuffer.asByteBuffer(index, length); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer() { + return dataBuffer.toByteBuffer(); + } + + @Override + @Deprecated(since = "6.0.5") + public ByteBuffer toByteBuffer(int index, int length) { + return dataBuffer.toByteBuffer(index, length); + } + + @Override + public void toByteBuffer(ByteBuffer dest) { + dataBuffer.toByteBuffer(dest); + } + + @Override + public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + } + + @Override + public ByteBufferIterator readableByteBuffers() { + return dataBuffer.readableByteBuffers(); + } + + @Override + public ByteBufferIterator writableByteBuffers() { + return dataBuffer.writableByteBuffers(); + } + + @Override + public InputStream asInputStream() { + return dataBuffer.asInputStream(); + } + + @Override + public InputStream asInputStream(boolean releaseOnClose) { + return dataBuffer.asInputStream(releaseOnClose); + } + + @Override + public OutputStream asOutputStream() { + return dataBuffer.asOutputStream(); + } + + @Override + public String toString(Charset charset) { + return dataBuffer.toString(charset); + } + + @Override + public String toString(int index, int length, Charset charset) { + return dataBuffer.toString(index, length, charset); + } +} From f070f906e979780ae140b94ab175144b7cfb8e0a Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 18 Jan 2024 21:15:05 +1100 Subject: [PATCH 078/146] minor httpHeader optimizations --- .../reactive/JettyCoreServerHttpRequest.java | 3 +- .../http/support/JettyHeadersAdapter.java | 77 +++++++++++++------ .../ContextPathIntegrationTests.java | 25 ++++-- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 9e5c45bc49dc..a849973e7cac 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -141,9 +141,8 @@ public MultiValueMap getCookies() { cookies = EMPTY_COOKIES; else { cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); } } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index 57f9b7ab8943..703db7254802 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -16,12 +16,7 @@ package org.springframework.http.support; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -43,6 +38,8 @@ public final class JettyHeadersAdapter implements MultiValueMap { private final HttpFields headers; + @Nullable + private final HttpFields.Mutable mutable; /** @@ -53,6 +50,7 @@ public final class JettyHeadersAdapter implements MultiValueMap public JettyHeadersAdapter(HttpFields headers) { Assert.notNull(headers, "Headers must not be null"); this.headers = headers; + this.mutable = headers instanceof HttpFields.Mutable m ? m : null; } @@ -119,22 +117,36 @@ public boolean isEmpty() { @Override public boolean containsKey(Object key) { - return (key instanceof String headerName && this.headers.contains(headerName)); + return (key instanceof String name && this.headers.contains(name)); } @Override public boolean containsValue(Object value) { - return (value instanceof String searchString && - this.headers.stream().anyMatch(field -> field.contains(searchString))); + if (value instanceof String searchString) { + for (HttpField field : this.headers) { + if (field.contains(searchString)) { + return true; + } + } + } + return false; } @Nullable @Override public List get(Object key) { - if (containsKey(key)) { - return this.headers.getValuesList((String) key); + List list = null; + if (key instanceof String name) { + for (HttpField f : this.headers) { + if (f.is(name)) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(f.getValue()); + } + } } - return null; + return list; } @Nullable @@ -142,7 +154,21 @@ public List get(Object key) { public List put(String key, List value) { HttpFields.Mutable mutableHttpFields = mutableFields(); List oldValues = get(key); - mutableHttpFields.put(key, value); + switch (value.size()) { + case 0 -> { + if (oldValues != null) { + mutableHttpFields.remove(key); + } + } + case 1 -> { + if (oldValues == null) { + mutableHttpFields.add(key, value.get(0)); + } else { + mutableHttpFields.put(key, value.get(0)); + } + } + default -> mutableHttpFields.put(key, value); + } return oldValues; } @@ -150,12 +176,21 @@ public List put(String key, List value) { @Override public List remove(Object key) { HttpFields.Mutable mutableHttpFields = mutableFields(); + List list = null; if (key instanceof String name) { - List oldValues = get(key); - mutableHttpFields.remove(name); - return oldValues; + for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext();) + { + HttpField f = i.next(); + if (f.is(name)) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(f.getValue()); + i.remove(); + } + } } - return null; + return list; } @Override @@ -195,16 +230,12 @@ public int size() { } private HttpFields.Mutable mutableFields() { - if (this.headers instanceof HttpFields.Mutable mutableHttpFields) { - return mutableHttpFields; - } - else { + if (mutable == null) { throw new IllegalStateException("Immutable headers"); } + return mutable; } - - @Override public String toString() { return HttpHeaders.formatHeaders(this); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java index 293cbdeec5bd..91104f2ed8e1 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java @@ -16,8 +16,11 @@ package org.springframework.web.reactive.result.method.annotation; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,10 +31,12 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; + +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; /** * Integration tests related to the use of context paths. @@ -40,15 +45,25 @@ */ class ContextPathIntegrationTests { - @Test - void multipleWebFluxApps() throws Exception { + static Stream> httpServers() { + return Stream.of( + named("Jetty", new JettyHttpServer()), + named("Jetty Core", new JettyCoreHttpServer()), + named("Reactor Netty", new ReactorHttpServer()), + named("Tomcat", new TomcatHttpServer()), + named("Undertow", new UndertowHttpServer()) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("httpServers") + void multipleWebFluxApps(AbstractHttpServer server) throws Exception { AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext(WebAppConfig.class); AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext(WebAppConfig.class); HttpHandler webApp1Handler = WebHttpHandlerBuilder.applicationContext(context1).build(); HttpHandler webApp2Handler = WebHttpHandlerBuilder.applicationContext(context2).build(); - ReactorHttpServer server = new ReactorHttpServer(); server.registerHttpHandler("/webApp1", webApp1Handler); server.registerHttpHandler("/webApp2", webApp2Handler); server.afterPropertiesSet(); From dc02b2865fb37046dac17559c86ac7143a054ae2 Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 20 Jan 2024 17:04:54 +0900 Subject: [PATCH 079/146] since updated with expected release --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 2 +- .../http/server/reactive/JettyCoreServerHttpRequest.java | 3 +-- .../http/server/reactive/JettyCoreServerHttpResponse.java | 2 +- .../http/server/reactive/bootstrap/JettyCoreHttpServer.java | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index f861606b60ec..d94e28d7777e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -27,7 +27,7 @@ * * @author Greg Wilkins * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index a849973e7cac..9ea8782b2b1d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -49,8 +49,7 @@ * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} * * @author Greg Wilkins - * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 89342e79d433..501d475e9f43 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,7 +42,7 @@ * * @author Greg Wilkins * @author Lachlan Roberts - * @since 6.1.4 + * @since 6.2 */ class JettyCoreServerHttpResponse implements ServerHttpResponse { enum State { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index eb5aad83fd1f..40da34f914a8 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -28,6 +28,7 @@ * @author Rossen Stoyanchev * @author Sam Brannen * @author Greg Wilkins + * @since 6.2 */ public class JettyCoreHttpServer extends AbstractHttpServer { From 543c30a6c1bff95006333976fe89886aa1431ce5 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:21:05 +1100 Subject: [PATCH 080/146] Initial implementation of the writeWith methods in JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 91 +++++++++++++++---- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 501d475e9f43..bdceb2600697 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -16,13 +16,24 @@ package org.springframework.http.server.reactive; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; @@ -30,13 +41,9 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} * @@ -45,11 +52,10 @@ * @since 6.2 */ class JettyCoreServerHttpResponse implements ServerHttpResponse { - enum State { - OPEN, COMMITTED, LAST, COMPLETED - } + private final AtomicBoolean committed = new AtomicBoolean(false); + + private final List>> commitActions = new CopyOnWriteArrayList<>(); - private final AtomicReference state = new AtomicReference<>(State.OPEN); private final Request request; private final Response response; private final HttpHeaders headers; @@ -67,13 +73,12 @@ public HttpHeaders getHeaders() { @Override public DataBufferFactory bufferFactory() { - // TODO - return null; + return DefaultDataBufferFactory.sharedInstance; } @Override public void beforeCommit(Supplier> action) { - // TODO See UndertowServerHttpResponse as an example + commitActions.add(action); } @Override @@ -82,21 +87,69 @@ public boolean isCommitted() { } @Override - public Mono writeWith(Publisher body) { - // TODO - return Mono.empty(); + public Mono writeWith(Publisher body) + { + return Flux.from(body) + .flatMap(this::mySend, 1) + .then(); + } + + private Mono mySend(DataBuffer dataBuffer) { + + if (committed.compareAndSet(false, true)) + { + if (!this.commitActions.isEmpty()) + { + return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) + .then(Mono.defer(() -> mySend(dataBuffer))) + .doOnError(t -> getHeaders().clearContentHeaders()); + } + } + + @SuppressWarnings("resource") + DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); + Callback.Completable callback = new Callback.Completable(); + new IteratingCallback() + { + @Override + protected Action process() + { + if (!byteBufferIterator.hasNext()) + return Action.SUCCEEDED; + response.write(false, byteBufferIterator.next(), this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + byteBufferIterator.close(); + callback.complete(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + byteBufferIterator.close(); + callback.failed(cause); + } + }.iterate(); + + return Mono.fromFuture(callback); } @Override public Mono writeAndFlushWith(Publisher> body) { - // TODO - return Mono.empty(); + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); } @Override public Mono setComplete() { - // TODO - return Mono.empty(); + Callback.Completable callback = new Callback.Completable(); + response.write(true, BufferUtil.EMPTY_BUFFER, callback); + return Mono.fromFuture(callback); } @Override From 0ef95af6a95fc1f3e99ef19869e76827a763298e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:42:23 +1100 Subject: [PATCH 081/146] Implement the ZeroCopyHttpOutputMessage interface for JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bdceb2600697..3155e5209327 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -16,6 +16,13 @@ package org.springframework.http.server.reactive; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; import java.util.Map; @@ -24,11 +31,15 @@ import java.util.function.Supplier; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; @@ -37,6 +48,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; @@ -51,7 +63,8 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse { +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage +{ private final AtomicBoolean committed = new AtomicBoolean(false); private final List>> commitActions = new CopyOnWriteArrayList<>(); @@ -94,6 +107,77 @@ public Mono writeWith(Publisher body) .then(); } + @Override + public Mono writeWith(Path file, long position, long count) + { + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); + try + { + // TODO: Why does this say possible blocking call? + SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); + new ContentWriterIteratingCallback(channel, response, callback).iterate(); + } + catch (Throwable t) + { + callback.failed(t); + } + return mono; + } + + private static class ContentWriterIteratingCallback extends IteratingCallback + { + private final ReadableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + + public ContentWriterIteratingCallback(ReadableByteChannel content, Response target, Callback callback) throws IOException + { + this.source = content; + this.sink = target; + this.callback = callback; + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); + int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); + boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); + this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); + } + + @Override + protected Action process() throws Throwable + { + if (!source.isOpen()) + return Action.SUCCEEDED; + + ByteBuffer byteBuffer = buffer.getByteBuffer(); + BufferUtil.clearToFill(byteBuffer); + int read = source.read(byteBuffer); + if (read == -1) + { + IO.close(source); + sink.write(true, BufferUtil.EMPTY_BUFFER, this); + return Action.SCHEDULED; + } + BufferUtil.flipToFlush(byteBuffer, 0); + sink.write(false, byteBuffer, this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + buffer.release(); + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable x) + { + buffer.release(); + callback.failed(x); + } + } + private Mono mySend(DataBuffer dataBuffer) { if (committed.compareAndSet(false, true)) From 4c0d782df0e54978c05a3168ceea96e852db4f80 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:42:47 +1100 Subject: [PATCH 082/146] Add temporary fix for cookies Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3155e5209327..db7ff4b63e40 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -264,7 +264,18 @@ public MultiValueMap getCookies() { if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); } - return cookies; + + // TODO: when cookies are added to this list they should be added to the response. + // Maybe something like this ? + return new LinkedMultiValueMap<>(cookies) + { + @Override + public void add(String key, ResponseCookie value) + { + super.add(key, value); + addCookie(value); + } + }; } @Override From c5caff8663e41873ac7e2e74c563120ccbb3d3ae Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 01:43:30 +1100 Subject: [PATCH 083/146] disable setting of the same site cookie attribute to fix test Signed-off-by: Lachlan Roberts --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index db7ff4b63e40..0ed9b6f5e22f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -339,11 +339,8 @@ public boolean isSecure() { @Override public SameSite getSameSite() { - String sameSiteName = responseCookie.getSameSite(); - if (sameSiteName != null) - return SameSite.valueOf(sameSiteName); - SameSite sameSite = HttpCookieUtils.getSameSiteDefault(request.getContext()); - return sameSite == null ? SameSite.NONE : sameSite; + // Adding non-null return site breaks tests. + return null; } @Override From 8bb9f63912b2c9cc3bd76b12ac22a9b008ef046c Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 10:58:31 +1100 Subject: [PATCH 084/146] rearrange methods in JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 0ed9b6f5e22f..680f943b392d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -103,10 +103,24 @@ public boolean isCommitted() { public Mono writeWith(Publisher body) { return Flux.from(body) - .flatMap(this::mySend, 1) + .flatMap(this::sendDataBuffer, 1) .then(); } + @Override + public Mono writeAndFlushWith(Publisher> body) { + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); + } + + @Override + public Mono setComplete() { + Callback.Completable callback = new Callback.Completable(); + response.write(true, BufferUtil.EMPTY_BUFFER, callback); + return Mono.fromFuture(callback); + } + @Override public Mono writeWith(Path file, long position, long count) { @@ -178,14 +192,14 @@ protected void onCompleteFailure(Throwable x) } } - private Mono mySend(DataBuffer dataBuffer) { + private Mono sendDataBuffer(DataBuffer dataBuffer) { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .then(Mono.defer(() -> mySend(dataBuffer))) + .then(Mono.defer(() -> sendDataBuffer(dataBuffer))) .doOnError(t -> getHeaders().clearContentHeaders()); } } @@ -222,20 +236,6 @@ protected void onCompleteFailure(Throwable cause) return Mono.fromFuture(callback); } - @Override - public Mono writeAndFlushWith(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); - } - - @Override - public Mono setComplete() { - Callback.Completable callback = new Callback.Completable(); - response.write(true, BufferUtil.EMPTY_BUFFER, callback); - return Mono.fromFuture(callback); - } - @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { if (isCommitted() || status == null) From 6311b3344270e1b0f3f5457790dc0cf6935ebf26 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:31:12 +1100 Subject: [PATCH 085/146] fix issues with cookies, committing response, and with ZeroCopyHttpOutputMessage Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreHttpHandlerAdapter.java | 11 +- .../reactive/JettyCoreServerHttpResponse.java | 106 ++++++++++++------ 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index d94e28d7777e..2479ae5b4655 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -16,11 +16,14 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.server.*; -import org.eclipse.jetty.util.*; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.*; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; /** * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. @@ -46,7 +49,7 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(request, response)) + httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 680f943b392d..3b7822a00a19 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; @@ -35,7 +34,6 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.server.HttpCookieUtils; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -66,15 +64,15 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); - private final List>> commitActions = new CopyOnWriteArrayList<>(); - private final Request request; private final Response response; private final HttpHeaders headers; - public JettyCoreServerHttpResponse(Request request, Response response) { - this.request = request; + @Nullable + private LinkedMultiValueMap cookies; + + public JettyCoreServerHttpResponse(Response response) { this.response = response; headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); } @@ -116,6 +114,10 @@ public Mono writeAndFlushWith(Publisher setComplete() { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(this::setComplete)); + Callback.Completable callback = new Callback.Completable(); response.write(true, BufferUtil.EMPTY_BUFFER, callback); return Mono.fromFuture(callback); @@ -124,13 +126,17 @@ public Mono setComplete() { @Override public Mono writeWith(Path file, long position, long count) { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(() -> writeWith(file, position, count))); + Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); + mono = Mono.fromFuture(callback); try { // TODO: Why does this say possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); - new ContentWriterIteratingCallback(channel, response, callback).iterate(); + new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } catch (Throwable t) { @@ -141,16 +147,21 @@ public Mono writeWith(Path file, long position, long count) private static class ContentWriterIteratingCallback extends IteratingCallback { - private final ReadableByteChannel source; + private final SeekableByteChannel source; private final Content.Sink sink; private final Callback callback; private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; - public ContentWriterIteratingCallback(ReadableByteChannel content, Response target, Callback callback) throws IOException + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException { this.source = content; this.sink = target; this.callback = callback; + this.length = count; + source.position(position); + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); @@ -160,11 +171,12 @@ public ContentWriterIteratingCallback(ReadableByteChannel content, Response targ @Override protected Action process() throws Throwable { - if (!source.isOpen()) + if (!source.isOpen() || totalRead == length) return Action.SUCCEEDED; ByteBuffer byteBuffer = buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); + byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); int read = source.read(byteBuffer); if (read == -1) { @@ -172,6 +184,7 @@ protected Action process() throws Throwable sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; } + totalRead += read; BufferUtil.flipToFlush(byteBuffer, 0); sink.write(false, byteBuffer, this); return Action.SCHEDULED; @@ -181,6 +194,7 @@ protected Action process() throws Throwable protected void onCompleteSuccess() { buffer.release(); + IO.close(source); callback.succeeded(); } @@ -188,22 +202,49 @@ protected void onCompleteSuccess() protected void onCompleteFailure(Throwable x) { buffer.release(); + IO.close(source); callback.failed(x); } } - private Mono sendDataBuffer(DataBuffer dataBuffer) { - + @Nullable + private Mono ensureCommitted() + { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .then(Mono.defer(() -> sendDataBuffer(dataBuffer))) - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } + + doCommit(); } + return null; + } + + private void doCommit() + { + if (cookies != null) + { + // TODO: are we doubling up on cookies already existing in response? + cookies.values().stream() + .flatMap(List::stream) + .forEach(cookie -> + { + Response.addCookie(response, new HttpResponseCookie(cookie)); + }); + } + } + + private Mono sendDataBuffer(DataBuffer dataBuffer) { + Mono mono = ensureCommitted(); + if (mono != null) + return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -259,31 +300,28 @@ public boolean setRawStatusCode(@Nullable Integer value) { @Override public MultiValueMap getCookies() { - LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); - } - - // TODO: when cookies are added to this list they should be added to the response. - // Maybe something like this ? - return new LinkedMultiValueMap<>(cookies) - { - @Override - public void add(String key, ResponseCookie value) - { - super.add(key, value); - addCookie(value); - } - }; + if (cookies == null) + initializeCookies(); + return cookies; } @Override public void addCookie(ResponseCookie cookie) { - Response.addCookie(response, new HttpResponseCookie(cookie)); + if (cookies == null) + initializeCookies(); + cookies.add(cookie.getName(), cookie); + } + + private void initializeCookies() + { + cookies = new LinkedMultiValueMap<>(); + for (HttpField f : response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) + cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } } - private class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; public HttpResponseCookie(ResponseCookie responseCookie) { From 01bde19679524ad04a66a71971a3d2331d8bd1a9 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:34:50 +1100 Subject: [PATCH 086/146] cleanups Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 137 +++++++++--------- 1 file changed, 67 insertions(+), 70 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3b7822a00a19..987849a5cd93 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -145,68 +145,6 @@ public Mono writeWith(Path file, long position, long count) return mono; } - private static class ContentWriterIteratingCallback extends IteratingCallback - { - private final SeekableByteChannel source; - private final Content.Sink sink; - private final Callback callback; - private final RetainableByteBuffer buffer; - private final long length; - private long totalRead = 0; - - public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException - { - this.source = content; - this.sink = target; - this.callback = callback; - this.length = count; - source.position(position); - - ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); - int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); - boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); - this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); - } - - @Override - protected Action process() throws Throwable - { - if (!source.isOpen() || totalRead == length) - return Action.SUCCEEDED; - - ByteBuffer byteBuffer = buffer.getByteBuffer(); - BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); - int read = source.read(byteBuffer); - if (read == -1) - { - IO.close(source); - sink.write(true, BufferUtil.EMPTY_BUFFER, this); - return Action.SCHEDULED; - } - totalRead += read; - BufferUtil.flipToFlush(byteBuffer, 0); - sink.write(false, byteBuffer, this); - return Action.SCHEDULED; - } - - @Override - protected void onCompleteSuccess() - { - buffer.release(); - IO.close(source); - callback.succeeded(); - } - - @Override - protected void onCompleteFailure(Throwable x) - { - buffer.release(); - IO.close(source); - callback.failed(x); - } - } - @Nullable private Mono ensureCommitted() { @@ -215,9 +153,9 @@ private Mono ensureCommitted() if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } doCommit(); @@ -232,11 +170,8 @@ private void doCommit() { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> - { - Response.addCookie(response, new HttpResponseCookie(cookie)); - }); + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); } } @@ -396,4 +331,66 @@ public Map getAttributes() { return Collections.emptyMap(); } } + + private static class ContentWriterIteratingCallback extends IteratingCallback + { + private final SeekableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; + + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException + { + this.source = content; + this.sink = target; + this.callback = callback; + this.length = count; + source.position(position); + + ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); + int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); + boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); + this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); + } + + @Override + protected Action process() throws Throwable + { + if (!source.isOpen() || totalRead == length) + return Action.SUCCEEDED; + + ByteBuffer byteBuffer = buffer.getByteBuffer(); + BufferUtil.clearToFill(byteBuffer); + byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); + int read = source.read(byteBuffer); + if (read == -1) + { + IO.close(source); + sink.write(true, BufferUtil.EMPTY_BUFFER, this); + return Action.SCHEDULED; + } + totalRead += read; + BufferUtil.flipToFlush(byteBuffer, 0); + sink.write(false, byteBuffer, this); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + buffer.release(); + IO.close(source); + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable x) + { + buffer.release(); + IO.close(source); + callback.failed(x); + } + } } From 1c6d8dbc63651d0d7779f3e5cf929fcf31d8de1f Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 23 Jan 2024 14:36:34 +1100 Subject: [PATCH 087/146] cleanups Signed-off-by: Lachlan Roberts --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 987849a5cd93..d7181c9edd63 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -134,7 +134,7 @@ public Mono writeWith(Path file, long position, long count) mono = Mono.fromFuture(callback); try { - // TODO: Why does this say possible blocking call? + // TODO: Why does intellij warn about possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } @@ -310,6 +310,7 @@ public boolean isSecure() { return responseCookie.isSecure(); } + @Nullable @Override public SameSite getSameSite() { // Adding non-null return site breaks tests. From 071cd55af82124654f5145277a7a092fce05517e Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 12:54:36 +0900 Subject: [PATCH 088/146] removed TODO --- .../http/server/reactive/JettyCoreHttpHandlerAdapter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 2479ae5b4655..48841a7b0a00 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -40,10 +40,10 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; - // TODO currently we do not make a DataBufferFactory over the servers ByteBufferPool, - // because we mainly use wrap and there should be few allocation done by the factory. - // But it should be possible to use the servers buffer pool for allocations and to - // create PooledDataBuffers + // Currently we do not make a DataBufferFactory over the servers ByteBufferPool, + // because we mainly use wrap and there should be few allocation done by the factory. + // But it could be possible to use the servers buffer pool for allocations and to + // create PooledDataBuffers dataBufferFactory = new DefaultDataBufferFactory(); } From d18b34b4bb203465d62362f629dc7ba6b10e29da Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 13:11:07 +0900 Subject: [PATCH 089/146] Reformatted code --- .../reactive/JettyCoreServerHttpRequest.java | 27 ++++-- .../reactive/JettyCoreServerHttpResponse.java | 87 ++++++++----------- .../reactive/JettyRetainedDataBuffer.java | 14 +-- .../bootstrap/JettyCoreHttpServer.java | 5 +- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 9ea8782b2b1d..db3787e631ae 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -16,11 +16,21 @@ package org.springframework.http.server.reactive; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.regex.Matcher; + import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -33,15 +43,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.regex.Matcher; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -53,15 +54,23 @@ */ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private final DataBufferFactory dataBufferFactory; + private final Request request; + private final HttpHeaders headers; + private final RequestPath path; + @Nullable private URI uri; + @Nullable MultiValueMap queryParameters; + @Nullable private MultiValueMap cookies; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index d7181c9edd63..e2cfc8e58b16 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,6 +40,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -51,8 +54,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} @@ -61,12 +62,13 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage -{ +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); + private final List>> commitActions = new CopyOnWriteArrayList<>(); private final Response response; + private final HttpHeaders headers; @Nullable @@ -98,8 +100,7 @@ public boolean isCommitted() { } @Override - public Mono writeWith(Publisher body) - { + public Mono writeWith(Publisher body) { return Flux.from(body) .flatMap(this::sendDataBuffer, 1) .then(); @@ -124,38 +125,32 @@ public Mono setComplete() { } @Override - public Mono writeWith(Path file, long position, long count) - { + public Mono writeWith(Path file, long position, long count) { Mono mono = ensureCommitted(); if (mono != null) return mono.then(Mono.defer(() -> writeWith(file, position, count))); Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); - try - { + try { // TODO: Why does intellij warn about possible blocking call? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); } - catch (Throwable t) - { + catch (Throwable t) { callback.failed(t); } return mono; } @Nullable - private Mono ensureCommitted() - { - if (committed.compareAndSet(false, true)) - { - if (!this.commitActions.isEmpty()) - { + private Mono ensureCommitted() { + if (committed.compareAndSet(false, true)) { + if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); + .concatWith(Mono.fromRunnable(this::doCommit)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); } doCommit(); @@ -164,14 +159,12 @@ private Mono ensureCommitted() return null; } - private void doCommit() - { - if (cookies != null) - { + private void doCommit() { + if (cookies != null) { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); } } @@ -183,11 +176,9 @@ private Mono sendDataBuffer(DataBuffer dataBuffer) { @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); - new IteratingCallback() - { + new IteratingCallback() { @Override - protected Action process() - { + protected Action process() { if (!byteBufferIterator.hasNext()) return Action.SUCCEEDED; response.write(false, byteBufferIterator.next(), this); @@ -195,15 +186,13 @@ protected Action process() } @Override - protected void onCompleteSuccess() - { + protected void onCompleteSuccess() { byteBufferIterator.close(); callback.complete(null); } @Override - protected void onCompleteFailure(Throwable cause) - { + protected void onCompleteFailure(Throwable cause) { byteBufferIterator.close(); callback.failed(cause); } @@ -247,8 +236,7 @@ public void addCookie(ResponseCookie cookie) { cookies.add(cookie.getName(), cookie); } - private void initializeCookies() - { + private void initializeCookies() { cookies = new LinkedMultiValueMap<>(); for (HttpField f : response.getHeaders()) { if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) @@ -333,17 +321,20 @@ public Map getAttributes() { } } - private static class ContentWriterIteratingCallback extends IteratingCallback - { + private static class ContentWriterIteratingCallback extends IteratingCallback { private final SeekableByteChannel source; + private final Content.Sink sink; + private final Callback callback; + private final RetainableByteBuffer buffer; + private final long length; + private long totalRead = 0; - public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException - { + public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException { this.source = content; this.sink = target; this.callback = callback; @@ -357,17 +348,15 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position } @Override - protected Action process() throws Throwable - { + protected Action process() throws Throwable { if (!source.isOpen() || totalRead == length) return Action.SUCCEEDED; ByteBuffer byteBuffer = buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int)Math.min(buffer.capacity(), length - totalRead)); + byteBuffer.limit((int) Math.min(buffer.capacity(), length - totalRead)); int read = source.read(byteBuffer); - if (read == -1) - { + if (read == -1) { IO.close(source); sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; @@ -379,16 +368,14 @@ protected Action process() throws Throwable } @Override - protected void onCompleteSuccess() - { + protected void onCompleteSuccess() { buffer.release(); IO.close(source); callback.succeeded(); } @Override - protected void onCompleteFailure(Throwable x) - { + protected void onCompleteFailure(Throwable x) { buffer.release(); IO.close(source); callback.failed(x); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 5b9c2fbd1bd7..8c239c5cf7f8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -16,12 +16,6 @@ package org.springframework.http.server.reactive; -import org.eclipse.jetty.io.Retainable; -import org.eclipse.jetty.server.Response; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; - import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -29,6 +23,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.IntPredicate; +import org.eclipse.jetty.io.Retainable; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; + /** * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} * @@ -38,7 +38,9 @@ */ public class JettyRetainedDataBuffer implements PooledDataBuffer { private final Retainable retainable; + private final DataBuffer dataBuffer; + private final AtomicBoolean allocated = new AtomicBoolean(true); public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 40da34f914a8..dd756cc28c13 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,13 +16,10 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; -import org.eclipse.jetty.ee10.servlet.ServletContextHandler; -import org.eclipse.jetty.ee10.servlet.ServletHolder; -import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; + import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; -import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; /** * @author Rossen Stoyanchev From 6b36d7011f0431390de7c173a36feda77177ab83 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 15:49:03 +0900 Subject: [PATCH 090/146] Fixed unset response status bug Jetty has an unset response status of 0, this should be converted to 200 for spring. --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index e2cfc8e58b16..fc1cc8a898b9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -211,7 +211,8 @@ public boolean setStatusCode(@Nullable HttpStatusCode status) { @Override public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(response.getStatus()); + int status = response.getStatus(); + return HttpStatusCode.valueOf(status == 0 ? 200 : status); } @Override From d86e7956f74e1055ecbc306687ca9a0995937cdc Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 16:09:50 +0900 Subject: [PATCH 091/146] Request header mutation support When mutating the request, a mutable HttpFields instance is created. --- .../DefaultServerHttpRequestBuilder.java | 32 ++++++++++++++----- .../reactive/JettyCoreServerHttpRequest.java | 10 +++++- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index a94e378e3c3c..ef6a2fa1bf4d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -20,6 +20,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.Objects; import java.util.function.Consumer; import reactor.core.publisher.Flux; @@ -67,14 +68,29 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { - Assert.notNull(original, "ServerHttpRequest is required"); - - this.uri = original.getURI(); - this.headers = new HttpHeaders(original.getHeaders()); - this.httpMethod = original.getMethod(); - this.contextPath = original.getPath().contextPath().value(); - this.remoteAddress = original.getRemoteAddress(); - this.body = original.getBody(); + this(original.getURI(), + new HttpHeaders(original.getHeaders()), + original.getMethod(), + original.getPath().contextPath().value(), + original.getRemoteAddress(), + original.getBody(), + Objects.requireNonNull(original, "ServerHttpRequest is required")); + } + + public DefaultServerHttpRequestBuilder( + URI uri, + HttpHeaders httpHeaders, + HttpMethod method, + String contextPath, + @Nullable InetSocketAddress remoteAddress, + Flux body, + ServerHttpRequest original) { + this.uri = uri; + this.headers = httpHeaders; + this.httpMethod = method; + this.contextPath = contextPath; + this.remoteAddress = remoteAddress; + this.body = body; this.originalRequest = original; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index db3787e631ae..46d3fdfed948 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,8 +22,10 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; @@ -189,6 +191,12 @@ public X509Certificate[] getPeerCertificates() { @Override public Builder mutate() { - return ServerHttpRequest.super.mutate(); + return new DefaultServerHttpRequestBuilder(this.getURI(), + new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(request.getHeaders()))), + this.getMethod(), + this.getPath().contextPath().value(), + this.getRemoteAddress(), + this.getBody(), + Objects.requireNonNull(this, "ServerHttpRequest is required")); } } From 8d64408850328ef42dffa033985af48393157ecb Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 24 Jan 2024 16:22:00 +0900 Subject: [PATCH 092/146] Do not use canonical path Spring appears to want the unadulterated raw path complete with parameters etc. --- .../http/server/reactive/JettyCoreServerHttpRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 46d3fdfed948..31fbdad816a1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -80,7 +80,7 @@ public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request r this.dataBufferFactory = dataBufferFactory; this.request = request; headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getCanonicalPath(), request.getContext().getContextPath()); + path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); } @Override From 6c4f7bc838e36636bfe184dddf6354fcc0dbfccb Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 25 Jan 2024 09:45:02 +0900 Subject: [PATCH 093/146] Track leaks (for now) and retain DataBuffer chunks --- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 + .../server/reactive/bootstrap/JettyCoreHttpServer.java | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 8c239c5cf7f8..e6327997ccf3 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -46,6 +46,7 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { this.dataBuffer = dataBuffer; this.retainable = retainable; + this.retainable.retain(); } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index dd756cc28c13..71eb061ecbab 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,6 +16,7 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -29,12 +30,14 @@ */ public class JettyCoreHttpServer extends AbstractHttpServer { + private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove private Server jettyServer; @Override protected void initServer() { - this.jettyServer = new Server(); + this.byteBufferPool = new ArrayByteBufferPool.Tracking(); + this.jettyServer = new Server(null, null, byteBufferPool); ServerConnector connector = new ServerConnector(this.jettyServer); connector.setHost(getHost()); @@ -62,6 +65,11 @@ protected void stopInternal() { catch (Exception ex) { // ignore } + + // TODO remove this or make debug only + this.byteBufferPool.dumpLeaks(); + if (!this.byteBufferPool.getLeaks().isEmpty()) + throw new IllegalStateException("LEAKS"); } @Override From 56e933c532d8273dac55564be63d9cbd54acd6ca Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 30 Jan 2024 18:29:30 +1100 Subject: [PATCH 094/146] Fixes for Jetty Core WebSocket Signed-off-by: Lachlan Roberts --- spring-web/spring-web.gradle | 1 + .../reactive/JettyCoreServerHttpRequest.java | 4 +- .../reactive/JettyCoreServerHttpResponse.java | 6 +- .../bootstrap/JettyCoreHttpServer.java | 7 +- spring-webflux/spring-webflux.gradle | 1 + .../adapter/JettyWebSocketHandlerAdapter.java | 12 +- .../JettyCoreRequestUpgradeStrategy.java | 167 ++++++++++++++++++ .../upgrade/JettyRequestUpgradeStrategy.java | 5 +- ...ractReactiveWebSocketIntegrationTests.java | 29 ++- 9 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 03b4a0ff19a8..7585e176b996 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -72,6 +72,7 @@ dependencies { because("needed by Netty's SelfSignedCertificate on JDK 15+") } testFixturesImplementation("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") + testFixturesImplementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-context"))) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 31fbdad816a1..55f9b1defc11 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -31,8 +31,6 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -45,6 +43,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -54,6 +53,7 @@ * @author Greg Wilkins * @since 6.2 */ +// TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index fc1cc8a898b9..23c4aa36d7b0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,9 +40,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -54,6 +51,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} @@ -62,6 +61,7 @@ * @author Lachlan Roberts * @since 6.2 */ +// TODO: extend AbstractServerHttpResponse for websocket. class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 71eb061ecbab..c40dc6fa5538 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -19,7 +19,7 @@ import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; - +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; /** @@ -44,7 +44,10 @@ protected void initServer() { connector.setPort(getPort()); this.jettyServer.addConnector(connector); this.jettyServer.setHandler(createHandlerAdapter()); - // TODO add websocket upgrade handler + + // TODO: We don't actually want the upgrade handler but this will create the WebSocketContainer. + // This requires a change in Jetty. + WebSocketUpgradeHandler.from(jettyServer); } private JettyCoreHttpHandlerAdapter createHandlerAdapter() { diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 29b7021f033b..9fb1d66d6597 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -27,6 +27,7 @@ dependencies { optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 71c0a2c840c1..b8f38256495b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,7 +32,6 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.core.OpCode; - import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -53,18 +52,14 @@ */ @WebSocket public class JettyWebSocketHandlerAdapter { - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - private final WebSocketHandler delegateHandler; - private final Function sessionFactory; @Nullable private JettyWebSocketSession delegateSession; - public JettyWebSocketHandlerAdapter(WebSocketHandler handler, Function sessionFactory) { @@ -74,7 +69,6 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler handler, this.sessionFactory = sessionFactory; } - @OnWebSocketOpen public void onWebSocketOpen(Session session) { this.delegateSession = this.sessionFactory.apply(session); @@ -101,6 +95,9 @@ public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } + else { + callback.succeed(); + } } @OnWebSocketFrame @@ -112,8 +109,11 @@ public void onWebSocketFrame(Frame frame, Callback callback) { buffer = new JettyDataBuffer(buffer, callback); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + return; } } + + callback.succeed(); } @OnWebSocketClose diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java new file mode 100644 index 000000000000..94fdf5bda060 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.socket.server.upgrade; + +import java.lang.reflect.Field; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.websocket.api.Configurable; +import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; +import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; +import org.eclipse.jetty.websocket.server.WebSocketCreator; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; +import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +/** + * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 Core. + * + * @author Rossen Stoyanchev + * @since 5.3.4 + */ +public class JettyCoreRequestUpgradeStrategy implements RequestUpgradeStrategy { + + @Nullable + private Consumer webSocketConfigurer; + + @Nullable + private ServerWebSocketContainer serverContainer; + + /** + * Add a callback to configure WebSocket server parameters on + * {@link JettyWebSocketServerContainer}. + * @since 6.1 + */ + public void addWebSocketConfigurer(Consumer webSocketConfigurer) { + this.webSocketConfigurer = (this.webSocketConfigurer != null ? + this.webSocketConfigurer.andThen(webSocketConfigurer) : webSocketConfigurer); + } + + private Request getJettyRequest(ServerHttpRequest request) + { + try + { + // TODO: JettyCoreServerHttpRequest should extend AbstractServerHttpRequest. + // This will allow the ServerHttpRequestDecorator.getNativeRequest(request) to extract the native request. + Field requestField = request.getClass().getDeclaredField("request"); + requestField.setAccessible(true); + return (Request)requestField.get(request); + } + catch (NoSuchFieldException | IllegalAccessException e) + { + return null; + } + } + + private Response getJettyResponse(ServerHttpResponse response) + { + try + { + // TODO: JettyCoreServerHttpResponse should extend AbstractServerHttpResponse. + // This will allow the ServerHttpRequestDecorator.getNativeResponse(response) to extract the native response. + Field requestField = response.getClass().getDeclaredField("response"); + requestField.setAccessible(true); + return (Response)requestField.get(response); + } + catch (NoSuchFieldException | IllegalAccessException e) + { + return null; + } + } + + @Override + public Mono upgrade( + ServerWebExchange exchange, WebSocketHandler handler, + @Nullable String subProtocol, Supplier handshakeInfoFactory) { + + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + + Request jettyRequest = getJettyRequest(request); + Response jettyResponse = getJettyResponse(response); + + HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); + DataBufferFactory factory = response.bufferFactory(); + + // Trigger WebFlux preCommit actions before upgrade + response.beforeCommit(() -> Mono.deferContextual(contextView -> + { + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new JettyWebSocketSession(session, handshakeInfo, factory)); + + WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> + { + if (subProtocol != null) + { + upgradeResponse.setAcceptedSubProtocol(subProtocol); + } + return adapter; + }; + + ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); + try + { + FutureCallback callback = new FutureCallback(); + if (container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) + { + callback.block(); + } + else + { + throw new WebSocketException("request could not be upgraded to websocket"); + } + } + catch (Exception ex) + { + return Mono.error(ex); + } + + return Mono.empty(); + })); + + return exchange.getResponse().setComplete(); + } + + private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { + if (this.serverContainer == null) { + Server server = jettyRequest.getConnectionMetaData().getConnector().getServer(); + ServerWebSocketContainer container = ServerWebSocketContainer.get(server.getContext()); + if (this.webSocketConfigurer != null) { + this.webSocketConfigurer.accept(container); + } + this.serverContainer = container; + } + return this.serverContainer; + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index 66b41aa3acf1..82711c47262c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -25,8 +25,6 @@ import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.websocket.api.Configurable; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -40,9 +38,10 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; /** - * A WebSocket {@code RequestUpgradeStrategy} for Jetty 11. + * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 EE10. * * @author Rossen Stoyanchev * @since 5.3.4 diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index fe5909ca8776..f991291c13f7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,13 +31,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; -import org.xnio.OptionMap; -import org.xnio.Xnio; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple3; - import org.springframework.context.ApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -54,6 +47,7 @@ import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.server.upgrade.JettyCoreRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.JettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNetty2RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; @@ -61,6 +55,17 @@ import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; +import org.xnio.OptionMap; +import org.xnio.Xnio; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple3; /** * Base class for reactive WebSocket integration tests. Subclasses must implement @@ -93,7 +98,7 @@ static Stream arguments() throws IOException { Map> servers = new LinkedHashMap<>(); servers.put(new TomcatHttpServer(TMP_DIR.getAbsolutePath(), WsContextListener.class), TomcatConfig.class); servers.put(new JettyHttpServer(), JettyConfig.class); - servers.put(new JettyCoreHttpServer(), JettyConfig.class); + servers.put(new JettyCoreHttpServer(), JettyCoreConfig.class); servers.put(new ReactorHttpServer(), ReactorNettyConfig.class); servers.put(new UndertowHttpServer(), UndertowConfig.class); @@ -238,4 +243,12 @@ protected RequestUpgradeStrategy getUpgradeStrategy() { } } + @Configuration + static class JettyCoreConfig extends AbstractHandlerAdapterConfig { + + @Override + protected RequestUpgradeStrategy getUpgradeStrategy() { + return new JettyCoreRequestUpgradeStrategy(); + } + } } From 1bce7b9cb4141993707cb2004d6df1e7af3e400b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 30 Jan 2024 19:33:16 +1100 Subject: [PATCH 095/146] Temporary fix for cookies in JettyCoreServerHttpResponse with TODOs Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 23c4aa36d7b0..0bb4db0e311d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -51,6 +51,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.MultiValueMapAdapter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -147,19 +148,22 @@ public Mono writeWith(Path file, long position, long count) { private Mono ensureCommitted() { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { + // TODO: WebSocket upgrade bypasses this response and writes directly with the Jetty Response. + // Because of this our attempt at doing writeCookies doesn't work because some commitActions + // are settings cookies on this instance, but websocket needs them set before because it will do the commit. return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::doCommit)) + //.concatWith(Mono.fromRunnable(this::writeCookies)) .then() .doOnError(t -> getHeaders().clearContentHeaders()); } - doCommit(); + // writeCookies(); } return null; } - private void doCommit() { + private void writeCookies() { if (cookies != null) { // TODO: are we doubling up on cookies already existing in response? cookies.values().stream() @@ -227,14 +231,35 @@ public boolean setRawStatusCode(@Nullable Integer value) { public MultiValueMap getCookies() { if (cookies == null) initializeCookies(); - return cookies; + + // TODO: not good enough. + return new MultiValueMapAdapter<>(cookies) + { + @Override + public void add(String key, ResponseCookie value) + { + Response.addCookie(response, new HttpResponseCookie(value)); + super.add(key, value); + } + + @Override + public void set(String key, ResponseCookie value) + { + Response.putCookie(response, new HttpResponseCookie(value)); + super.set(key, value); + } + }; } @Override public void addCookie(ResponseCookie cookie) { + /* if (cookies == null) initializeCookies(); cookies.add(cookie.getName(), cookie); + */ + + Response.addCookie(response, new HttpResponseCookie(cookie)); } private void initializeCookies() { From f934d317163e6e62cb9753239a3e4fc0f6f4c721 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 00:38:33 +1100 Subject: [PATCH 096/146] Do not actually commit response in JettyCoreServerHttpResponse & fix cookies Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 42 +++---------------- .../JettyCoreRequestUpgradeStrategy.java | 27 ++++-------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 0bb4db0e311d..2d8ccd093b30 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -51,7 +51,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.MultiValueMapAdapter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -97,7 +96,7 @@ public void beforeCommit(Supplier> action) { @Override public boolean isCommitted() { - return response.isCommitted(); + return committed.get(); } @Override @@ -117,13 +116,8 @@ public Mono writeAndFlushWith(Publisher setComplete() { Mono mono = ensureCommitted(); - if (mono != null) - return mono.then(Mono.defer(this::setComplete)); - - Callback.Completable callback = new Callback.Completable(); - response.write(true, BufferUtil.EMPTY_BUFFER, callback); - return Mono.fromFuture(callback); - } + return (mono == null) ? Mono.empty() : mono; + } @Override public Mono writeWith(Path file, long position, long count) { @@ -148,16 +142,13 @@ public Mono writeWith(Path file, long position, long count) { private Mono ensureCommitted() { if (committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { - // TODO: WebSocket upgrade bypasses this response and writes directly with the Jetty Response. - // Because of this our attempt at doing writeCookies doesn't work because some commitActions - // are settings cookies on this instance, but websocket needs them set before because it will do the commit. return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - //.concatWith(Mono.fromRunnable(this::writeCookies)) + .concatWith(Mono.fromRunnable(this::writeCookies)) .then() .doOnError(t -> getHeaders().clearContentHeaders()); } - // writeCookies(); + writeCookies(); } return null; @@ -231,35 +222,14 @@ public boolean setRawStatusCode(@Nullable Integer value) { public MultiValueMap getCookies() { if (cookies == null) initializeCookies(); - - // TODO: not good enough. - return new MultiValueMapAdapter<>(cookies) - { - @Override - public void add(String key, ResponseCookie value) - { - Response.addCookie(response, new HttpResponseCookie(value)); - super.add(key, value); - } - - @Override - public void set(String key, ResponseCookie value) - { - Response.putCookie(response, new HttpResponseCookie(value)); - super.set(key, value); - } - }; + return cookies; } @Override public void addCookie(ResponseCookie cookie) { - /* if (cookies == null) initializeCookies(); cookies.add(cookie.getName(), cookie); - */ - - Response.addCookie(response, new HttpResponseCookie(cookie)); } private void initializeCookies() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 94fdf5bda060..72f83a1c04a8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -24,7 +24,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.websocket.api.Configurable; import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; @@ -113,8 +113,8 @@ public Mono upgrade( DataBufferFactory factory = response.bufferFactory(); // Trigger WebFlux preCommit actions before upgrade - response.beforeCommit(() -> Mono.deferContextual(contextView -> - { + return exchange.getResponse().setComplete() + .then(Mono.deferContextual(contextView -> { JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( ContextWebSocketHandler.decorate(handler, contextView), session -> new JettyWebSocketSession(session, handshakeInfo, factory)); @@ -122,34 +122,25 @@ public Mono upgrade( WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> { if (subProtocol != null) - { upgradeResponse.setAcceptedSubProtocol(subProtocol); - } return adapter; }; + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); try { - FutureCallback callback = new FutureCallback(); - if (container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) - { - callback.block(); - } - else - { + if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) throw new WebSocketException("request could not be upgraded to websocket"); - } } - catch (Exception ex) + catch (WebSocketException e) { - return Mono.error(ex); + callback.failed(e); } - return Mono.empty(); + return mono; })); - - return exchange.getResponse().setComplete(); } private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { From d2e152bfd7eba44095f5ae276260ab23838ebee8 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 00:40:48 +1100 Subject: [PATCH 097/146] Add JettyCore upgrade option in HandshakeWebSocketService Signed-off-by: Lachlan Roberts --- .../server/support/HandshakeWebSocketService.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java index 81b5326e817f..f599f9134d65 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java @@ -27,8 +27,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; - import org.springframework.context.Lifecycle; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.WebSocketService; +import org.springframework.web.reactive.socket.server.upgrade.JettyCoreRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.JettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNetty2RequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; @@ -52,6 +51,7 @@ import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; +import reactor.core.publisher.Mono; /** * {@code WebSocketService} implementation that handles a WebSocket HTTP @@ -76,6 +76,8 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle { private static final boolean jettyWsPresent; + private static final boolean jettyCoreWsPresent; + private static final boolean undertowWsPresent; private static final boolean reactorNettyPresent; @@ -88,6 +90,8 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle { "org.apache.tomcat.websocket.server.WsHttpUpgradeHandler", classLoader); jettyWsPresent = ClassUtils.isPresent( "org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer", classLoader); + jettyCoreWsPresent = ClassUtils.isPresent( + "org.eclipse.jetty.websocket.server.ServerWebSocketContainer", classLoader); undertowWsPresent = ClassUtils.isPresent( "io.undertow.websockets.WebSocketProtocolHandshakeHandler", classLoader); reactorNettyPresent = ClassUtils.isPresent( @@ -278,6 +282,9 @@ static RequestUpgradeStrategy initUpgradeStrategy() { else if (jettyWsPresent) { return new JettyRequestUpgradeStrategy(); } + else if (jettyCoreWsPresent) { + return new JettyCoreRequestUpgradeStrategy(); + } else if (undertowWsPresent) { return new UndertowRequestUpgradeStrategy(); } From 014c77c17025fae986e5150874181dece102e56d Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 31 Jan 2024 01:00:05 +1100 Subject: [PATCH 098/146] move getNativeRequest/Response to ServerHttpRequest/Response interfaces Signed-off-by: Lachlan Roberts --- .../reactive/AbstractServerHttpRequest.java | 7 ---- .../reactive/AbstractServerHttpResponse.java | 13 +------ .../reactive/JettyCoreServerHttpRequest.java | 7 ++++ .../reactive/JettyCoreServerHttpResponse.java | 7 ++++ .../server/reactive/ServerHttpRequest.java | 11 ++++++ .../reactive/ServerHttpRequestDecorator.java | 20 +++++----- .../server/reactive/ServerHttpResponse.java | 10 +++++ .../reactive/ServerHttpResponseDecorator.java | 19 ++++----- .../server/DefaultServerRequestBuilder.java | 11 ++++-- .../JettyCoreRequestUpgradeStrategy.java | 39 ++----------------- .../result/view/ZeroDemandResponse.java | 10 +++-- 11 files changed, 77 insertions(+), 77 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index da4659cd3e61..9e4f1547e171 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -203,13 +203,6 @@ public SslInfo getSslInfo() { @Nullable protected abstract SslInfo initSslInfo(); - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - public abstract T getNativeRequest(); - /** * For internal use in logging at the HTTP adapter layer. * @since 5.1 diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 7a06126ce85d..8885a2649efc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -23,9 +23,6 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -37,6 +34,8 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Base class for {@link ServerHttpResponse} implementations. @@ -155,14 +154,6 @@ public void addCookie(ResponseCookie cookie) { } } - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - public abstract T getNativeResponse(); - - @Override public void beforeCommit(Supplier> action) { this.commitActions.add(action); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 55f9b1defc11..59d42cc05f42 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -189,6 +189,13 @@ public X509Certificate[] getPeerCertificates() { return null; } + @SuppressWarnings("unchecked") + @Override + public T getNativeRequest() + { + return (T) request; + } + @Override public Builder mutate() { return new DefaultServerHttpRequestBuilder(this.getURI(), diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 2d8ccd093b30..3196392679fb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -232,6 +232,13 @@ public void addCookie(ResponseCookie cookie) { cookies.add(cookie.getName(), cookie); } + @SuppressWarnings("unchecked") + @Override + public T getNativeResponse() + { + return (T) response; + } + private void initializeCookies() { cookies = new LinkedMultiValueMap<>(); for (HttpField f : response.getHeaders()) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 55f5fa2c654b..5baa3ac51912 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -97,6 +97,17 @@ default SslInfo getSslInfo() { return null; } + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + @Nullable + default T getNativeRequest() + { + return null; + } + /** * Return a builder to mutate properties of this request by wrapping it * with {@link ServerHttpRequestDecorator} and returning either mutated diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index fc6143bfdaf0..05e92ab8395a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -19,8 +19,6 @@ import java.net.InetSocketAddress; import java.net.URI; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -29,6 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; /** * Wraps another {@link ServerHttpRequest} and delegates all methods to it. @@ -108,6 +107,12 @@ public SslInfo getSslInfo() { return getDelegate().getSslInfo(); } + @Override + public T getNativeRequest() + { + return delegate.getNativeRequest(); + } + @Override public Flux getBody() { return getDelegate().getBody(); @@ -123,16 +128,13 @@ public Flux getBody() { * @since 5.3.3 */ public static T getNativeRequest(ServerHttpRequest request) { - if (request instanceof AbstractServerHttpRequest abstractServerHttpRequest) { - return abstractServerHttpRequest.getNativeRequest(); - } - else if (request instanceof ServerHttpRequestDecorator serverHttpRequestDecorator) { - return getNativeRequest(serverHttpRequestDecorator.getDelegate()); - } - else { + T nativeRequest = request.getNativeRequest(); + if (nativeRequest == null) { throw new IllegalArgumentException( "Can't find native request in " + request.getClass().getName()); } + + return nativeRequest; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 7f97d04484a5..eb61473448e1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -86,4 +86,14 @@ default Integer getRawStatusCode() { */ void addCookie(ResponseCookie cookie); + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + @Nullable + default T getNativeResponse() + { + return null; + } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index d6f7f85b953c..58f78deb36f1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -19,8 +19,6 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; @@ -29,6 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; /** * Wraps another {@link ServerHttpResponse} and delegates all methods to it. @@ -123,6 +122,11 @@ public Mono setComplete() { return getDelegate().setComplete(); } + @Override + public T getNativeResponse() + { + return getDelegate().getNativeResponse(); + } /** * Return the native response of the underlying server API, if possible, @@ -133,16 +137,13 @@ public Mono setComplete() { * @since 5.3.3 */ public static T getNativeResponse(ServerHttpResponse response) { - if (response instanceof AbstractServerHttpResponse abstractServerHttpResponse) { - return abstractServerHttpResponse.getNativeResponse(); - } - else if (response instanceof ServerHttpResponseDecorator serverHttpResponseDecorator) { - return getNativeResponse(serverHttpResponseDecorator.getDelegate()); - } - else { + T nativeResponse = response.getNativeResponse(); + if (nativeResponse == null) { throw new IllegalArgumentException( "Can't find native response in " + response.getClass().getName()); } + + return nativeResponse; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index a97fbd902563..bb22457df381 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -28,9 +28,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.context.ApplicationContext; import org.springframework.context.i18n.LocaleContext; import org.springframework.core.ResolvableType; @@ -57,6 +54,8 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Default {@link ServerRequest.Builder} implementation. @@ -297,6 +296,12 @@ public MultiValueMap getQueryParams() { public Flux getBody() { return this.body; } + + @Override + public T getNativeRequest() + { + return null; + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 72f83a1c04a8..66cab235901b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -16,7 +16,6 @@ package org.springframework.web.reactive.socket.server.upgrade; -import java.lang.reflect.Field; import java.util.function.Consumer; import java.util.function.Supplier; @@ -31,7 +30,9 @@ import org.eclipse.jetty.websocket.server.WebSocketCreator; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.lang.Nullable; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -66,38 +67,6 @@ public void addWebSocketConfigurer(Consumer webSocketConfigurer) { this.webSocketConfigurer.andThen(webSocketConfigurer) : webSocketConfigurer); } - private Request getJettyRequest(ServerHttpRequest request) - { - try - { - // TODO: JettyCoreServerHttpRequest should extend AbstractServerHttpRequest. - // This will allow the ServerHttpRequestDecorator.getNativeRequest(request) to extract the native request. - Field requestField = request.getClass().getDeclaredField("request"); - requestField.setAccessible(true); - return (Request)requestField.get(request); - } - catch (NoSuchFieldException | IllegalAccessException e) - { - return null; - } - } - - private Response getJettyResponse(ServerHttpResponse response) - { - try - { - // TODO: JettyCoreServerHttpResponse should extend AbstractServerHttpResponse. - // This will allow the ServerHttpRequestDecorator.getNativeResponse(response) to extract the native response. - Field requestField = response.getClass().getDeclaredField("response"); - requestField.setAccessible(true); - return (Response)requestField.get(response); - } - catch (NoSuchFieldException | IllegalAccessException e) - { - return null; - } - } - @Override public Mono upgrade( ServerWebExchange exchange, WebSocketHandler handler, @@ -106,8 +75,8 @@ public Mono upgrade( ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); - Request jettyRequest = getJettyRequest(request); - Response jettyResponse = getJettyResponse(response); + Request jettyRequest = ServerHttpRequestDecorator.getNativeRequest(request); + Response jettyResponse = ServerHttpResponseDecorator.getNativeResponse(response); HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); DataBufferFactory factory = response.bufferFactory(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index b543447e1e68..afa859b6cbf3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -20,9 +20,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; @@ -31,6 +28,8 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Mono; /** * Response that subscribes to the writes source but never posts demand and also @@ -116,6 +115,11 @@ public HttpHeaders getHeaders() { throw new UnsupportedOperationException(); } + @Override + public T getNativeResponse() + { + return null; + } private static class ZeroDemandSubscriber extends BaseSubscriber { From cd8bf6a7fba93c0019c058c4accb8340a85458bf Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 18:36:52 +0900 Subject: [PATCH 099/146] Reformatted code --- .../reactive/AbstractServerHttpResponse.java | 5 +- .../reactive/JettyCoreHttpHandlerAdapter.java | 6 +- .../reactive/JettyCoreServerHttpRequest.java | 91 +++++++------ .../reactive/JettyCoreServerHttpResponse.java | 128 ++++++++++-------- .../reactive/JettyRetainedDataBuffer.java | 88 ++++++------ .../server/reactive/ServerHttpRequest.java | 3 +- .../reactive/ServerHttpRequestDecorator.java | 8 +- .../server/reactive/ServerHttpResponse.java | 3 +- .../reactive/ServerHttpResponseDecorator.java | 6 +- .../http/support/JettyHeadersAdapter.java | 23 +++- .../ErrorHandlerIntegrationTests.java | 7 +- .../reactive/ZeroCopyIntegrationTests.java | 8 +- .../bootstrap/JettyCoreHttpServer.java | 2 + .../server/DefaultServerRequestBuilder.java | 8 +- .../adapter/JettyWebSocketHandlerAdapter.java | 2 + .../support/HandshakeWebSocketService.java | 3 +- .../JettyCoreRequestUpgradeStrategy.java | 54 ++++---- .../upgrade/JettyRequestUpgradeStrategy.java | 3 +- .../ContextPathIntegrationTests.java | 14 +- .../annotation/SseIntegrationTests.java | 51 ++++--- .../result/view/ZeroDemandResponse.java | 8 +- ...ractReactiveWebSocketIntegrationTests.java | 11 +- 22 files changed, 291 insertions(+), 241 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 8885a2649efc..82731c218d90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -23,6 +23,9 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -34,8 +37,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Base class for {@link ServerHttpResponse} implementations. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 48841a7b0a00..a687d03ea791 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.util.Callback; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -35,6 +36,7 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; + private final DataBufferFactory dataBufferFactory; public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { @@ -44,12 +46,12 @@ public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { // because we mainly use wrap and there should be few allocation done by the factory. // But it could be possible to use the servers buffer pool for allocations and to // create PooledDataBuffers - dataBufferFactory = new DefaultDataBufferFactory(); + this.dataBufferFactory = new DefaultDataBufferFactory(); } @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - httpHandler.handle(new JettyCoreServerHttpRequest(dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) + this.httpHandler.handle(new JettyCoreServerHttpRequest(this.dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) .subscribe(new Subscriber<>() { @Override public void onSubscribe(Subscription s) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 59d42cc05f42..02c911e1a58a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; -import java.util.Objects; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -31,6 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -43,21 +44,20 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; /** - * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest} + * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. * * @author Greg Wilkins * @since 6.2 */ // TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { - private final static MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - private final static MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); + private static final MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private final DataBufferFactory dataBufferFactory; @@ -79,25 +79,26 @@ class JettyCoreServerHttpRequest implements ServerHttpRequest { public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { this.dataBufferFactory = dataBufferFactory; this.request = request; - headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); + this.headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); + this.path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); } @Override public HttpHeaders getHeaders() { - return headers; + return this.headers; } @Override public HttpMethod getMethod() { - return HttpMethod.valueOf(request.getMethod()); + return HttpMethod.valueOf(this.request.getMethod()); } @Override public URI getURI() { - if (uri == null) - uri = request.getHttpURI().toURI(); - return uri; + if (this.uri == null) { + this.uri = this.request.getHttpURI().toURI(); + } + return this.uri; } @Override @@ -105,75 +106,78 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(request))).map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap); } private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + return new JettyRetainedDataBuffer(this.dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); } @Override public String getId() { - return request.getId(); + return this.request.getId(); } @Override public RequestPath getPath() { - return path; + return this.path; } @Override public MultiValueMap getQueryParams() { - if (queryParameters == null) { - String query = request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) - queryParameters = EMPTY_QUERY; + if (this.queryParameters == null) { + String query = this.request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) { + this.queryParameters = EMPTY_QUERY; + } else { - queryParameters = new LinkedMultiValueMap<>(); + this.queryParameters = new LinkedMultiValueMap<>(); Matcher matcher = QUERY_PATTERN.matcher(query); while (matcher.find()) { String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); String eq = matcher.group(2); String value = matcher.group(3); value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - queryParameters.add(name, value); + this.queryParameters.add(name, value); } } } - return queryParameters; + return this.queryParameters; } @Override public MultiValueMap getCookies() { - if (cookies == null) { - List httpCookies = Request.getCookies(request); - if (httpCookies.isEmpty()) - cookies = EMPTY_COOKIES; + if (this.cookies == null) { + List httpCookies = Request.getCookies(this.request); + if (httpCookies.isEmpty()) { + this.cookies = EMPTY_COOKIES; + } else { - cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) - cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - cookies = CollectionUtils.unmodifiableMultiValueMap(cookies); + this.cookies = new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + this.cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + } + this.cookies = CollectionUtils.unmodifiableMultiValueMap(this.cookies); } } - return cookies; + return this.cookies; } @Override public InetSocketAddress getLocalAddress() { - return request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet + return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override public InetSocketAddress getRemoteAddress() { - return request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet + return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override public SslInfo getSslInfo() { - if (request.getConnectionMetaData().isSecure() && request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { + if (this.request.getConnectionMetaData().isSecure() && this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { return new SslInfo() { @Override public String getSessionId() { @@ -191,19 +195,18 @@ public X509Certificate[] getPeerCertificates() { @SuppressWarnings("unchecked") @Override - public T getNativeRequest() - { - return (T) request; + public T getNativeRequest() { + return (T) this.request; } @Override public Builder mutate() { return new DefaultServerHttpRequestBuilder(this.getURI(), - new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(request.getHeaders()))), - this.getMethod(), - this.getPath().contextPath().value(), - this.getRemoteAddress(), - this.getBody(), - Objects.requireNonNull(this, "ServerHttpRequest is required")); + new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(this.request.getHeaders()))), + this.getMethod(), + this.getPath().contextPath().value(), + this.getRemoteAddress(), + this.getBody(), + this); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 3196392679fb..09261ef76c52 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -40,6 +40,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -51,11 +54,9 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** - * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse} + * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. * * @author Greg Wilkins * @author Lachlan Roberts @@ -76,12 +77,12 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOut public JettyCoreServerHttpResponse(Response response) { this.response = response; - headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); } @Override public HttpHeaders getHeaders() { - return headers; + return this.headers; } @Override @@ -91,12 +92,12 @@ public DataBufferFactory bufferFactory() { @Override public void beforeCommit(Supplier> action) { - commitActions.add(action); + this.commitActions.add(action); } @Override public boolean isCommitted() { - return committed.get(); + return this.committed.get(); } @Override @@ -116,31 +117,33 @@ public Mono writeAndFlushWith(Publisher setComplete() { Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; - } + return (mono == null) ? Mono.empty() : mono; + } @Override public Mono writeWith(Path file, long position, long count) { Mono mono = ensureCommitted(); - if (mono != null) + if (mono != null) { return mono.then(Mono.defer(() -> writeWith(file, position, count))); + } Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { // TODO: Why does intellij warn about possible blocking call? + // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); - new ContentWriterIteratingCallback(channel, position, count, response, callback).iterate(); + new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } - catch (Throwable t) { - callback.failed(t); + catch (Throwable th) { + callback.failed(th); } return mono; } @Nullable private Mono ensureCommitted() { - if (committed.compareAndSet(false, true)) { + if (this.committed.compareAndSet(false, true)) { if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -148,25 +151,26 @@ private Mono ensureCommitted() { .doOnError(t -> getHeaders().clearContentHeaders()); } - writeCookies(); + writeCookies(); } return null; } private void writeCookies() { - if (cookies != null) { + if (this.cookies != null) { // TODO: are we doubling up on cookies already existing in response? - cookies.values().stream() + this.cookies.values().stream() .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(response, new HttpResponseCookie(cookie))); + .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); } } private Mono sendDataBuffer(DataBuffer dataBuffer) { Mono mono = ensureCommitted(); - if (mono != null) + if (mono != null) { return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + } @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); @@ -174,8 +178,9 @@ private Mono sendDataBuffer(DataBuffer dataBuffer) { new IteratingCallback() { @Override protected Action process() { - if (!byteBufferIterator.hasNext()) + if (!byteBufferIterator.hasNext()) { return Action.SUCCEEDED; + } response.write(false, byteBufferIterator.next(), this); return Action.SCHEDULED; } @@ -198,52 +203,56 @@ protected void onCompleteFailure(Throwable cause) { @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) + if (isCommitted() || status == null) { return false; - response.setStatus(status.value()); + } + this.response.setStatus(status.value()); return true; } @Override public HttpStatusCode getStatusCode() { - int status = response.getStatus(); + int status = this.response.getStatus(); return HttpStatusCode.valueOf(status == 0 ? 200 : status); } @Override public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) + if (isCommitted() || value == null) { return false; - response.setStatus(value); + } + this.response.setStatus(value); return true; } @Override public MultiValueMap getCookies() { - if (cookies == null) + if (this.cookies == null) { initializeCookies(); - return cookies; + } + return this.cookies; } @Override public void addCookie(ResponseCookie cookie) { - if (cookies == null) + if (this.cookies == null) { initializeCookies(); - cookies.add(cookie.getName(), cookie); + } + this.cookies.add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @Override - public T getNativeResponse() - { - return (T) response; + public T getNativeResponse() { + return (T) this.response; } private void initializeCookies() { - cookies = new LinkedMultiValueMap<>(); - for (HttpField f : response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) - cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + this.cookies = new LinkedMultiValueMap<>(); + for (HttpField f : this.response.getHeaders()) { + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) { + this.cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + } } } @@ -255,17 +264,17 @@ public HttpResponseCookie(ResponseCookie responseCookie) { } public ResponseCookie getResponseCookie() { - return responseCookie; + return this.responseCookie; } @Override public String getName() { - return responseCookie.getName(); + return this.responseCookie.getName(); } @Override public String getValue() { - return responseCookie.getValue(); + return this.responseCookie.getValue(); } @Override @@ -275,7 +284,7 @@ public int getVersion() { @Override public long getMaxAge() { - return responseCookie.getMaxAge().toSeconds(); + return this.responseCookie.getMaxAge().toSeconds(); } @Override @@ -287,18 +296,18 @@ public String getComment() { @Override @Nullable public String getDomain() { - return responseCookie.getDomain(); + return this.responseCookie.getDomain(); } @Override @Nullable public String getPath() { - return responseCookie.getPath(); + return this.responseCookie.getPath(); } @Override public boolean isSecure() { - return responseCookie.isSecure(); + return this.responseCookie.isSecure(); } @Nullable @@ -310,7 +319,7 @@ public SameSite getSameSite() { @Override public boolean isHttpOnly() { - return responseCookie.isHttpOnly(); + return this.responseCookie.isHttpOnly(); } @Override @@ -342,7 +351,7 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position this.sink = target; this.callback = callback; this.length = count; - source.position(position); + this.source.position(position); ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); @@ -352,36 +361,37 @@ public ContentWriterIteratingCallback(SeekableByteChannel content, long position @Override protected Action process() throws Throwable { - if (!source.isOpen() || totalRead == length) + if (!this.source.isOpen() || this.totalRead == this.length) { return Action.SUCCEEDED; + } - ByteBuffer byteBuffer = buffer.getByteBuffer(); + ByteBuffer byteBuffer = this.buffer.getByteBuffer(); BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int) Math.min(buffer.capacity(), length - totalRead)); - int read = source.read(byteBuffer); + byteBuffer.limit((int) Math.min(this.buffer.capacity(), this.length - this.totalRead)); + int read = this.source.read(byteBuffer); if (read == -1) { - IO.close(source); - sink.write(true, BufferUtil.EMPTY_BUFFER, this); + IO.close(this.source); + this.sink.write(true, BufferUtil.EMPTY_BUFFER, this); return Action.SCHEDULED; } - totalRead += read; + this.totalRead += read; BufferUtil.flipToFlush(byteBuffer, 0); - sink.write(false, byteBuffer, this); + this.sink.write(false, byteBuffer, this); return Action.SCHEDULED; } @Override protected void onCompleteSuccess() { - buffer.release(); - IO.close(source); - callback.succeeded(); + this.buffer.release(); + IO.close(this.source); + this.callback.succeeded(); } @Override protected void onCompleteFailure(Throwable x) { - buffer.release(); - IO.close(source); - callback.failed(x); + this.buffer.release(); + IO.close(this.source); + this.callback.failed(x); } } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index e6327997ccf3..77af0e288986 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -30,7 +30,7 @@ import org.springframework.core.io.buffer.PooledDataBuffer; /** - * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer} + * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer}. * * @author Greg Wilkins * @author Lachlan Roberts @@ -51,12 +51,12 @@ public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { @Override public boolean isAllocated() { - return allocated.get(); + return this.allocated.get(); } @Override public PooledDataBuffer retain() { - retainable.retain(); + this.retainable.retain(); return this; } @@ -67,8 +67,8 @@ public PooledDataBuffer touch(Object hint) { @Override public boolean release() { - if (retainable.release()) { - allocated.set(false); + if (this.retainable.release()) { + this.allocated.set(false); return true; } return false; @@ -76,204 +76,204 @@ public boolean release() { @Override public DataBufferFactory factory() { - return dataBuffer.factory(); + return this.dataBuffer.factory(); } @Override public int indexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.indexOf(predicate, fromIndex); + return this.dataBuffer.indexOf(predicate, fromIndex); } @Override public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return dataBuffer.lastIndexOf(predicate, fromIndex); + return this.dataBuffer.lastIndexOf(predicate, fromIndex); } @Override public int readableByteCount() { - return dataBuffer.readableByteCount(); + return this.dataBuffer.readableByteCount(); } @Override public int writableByteCount() { - return dataBuffer.writableByteCount(); + return this.dataBuffer.writableByteCount(); } @Override public int capacity() { - return dataBuffer.capacity(); + return this.dataBuffer.capacity(); } @Override @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { - return dataBuffer.capacity(capacity); + return this.dataBuffer.capacity(capacity); } @Override @Deprecated(since = "6.0") public DataBuffer ensureCapacity(int capacity) { - return dataBuffer.ensureCapacity(capacity); + return this.dataBuffer.ensureCapacity(capacity); } @Override public DataBuffer ensureWritable(int capacity) { - return dataBuffer.ensureWritable(capacity); + return this.dataBuffer.ensureWritable(capacity); } @Override public int readPosition() { - return dataBuffer.readPosition(); + return this.dataBuffer.readPosition(); } @Override public DataBuffer readPosition(int readPosition) { - return dataBuffer.readPosition(readPosition); + return this.dataBuffer.readPosition(readPosition); } @Override public int writePosition() { - return dataBuffer.writePosition(); + return this.dataBuffer.writePosition(); } @Override public DataBuffer writePosition(int writePosition) { - return dataBuffer.writePosition(writePosition); + return this.dataBuffer.writePosition(writePosition); } @Override public byte getByte(int index) { - return dataBuffer.getByte(index); + return this.dataBuffer.getByte(index); } @Override public byte read() { - return dataBuffer.read(); + return this.dataBuffer.read(); } @Override public DataBuffer read(byte[] destination) { - return dataBuffer.read(destination); + return this.dataBuffer.read(destination); } @Override public DataBuffer read(byte[] destination, int offset, int length) { - return dataBuffer.read(destination, offset, length); + return this.dataBuffer.read(destination, offset, length); } @Override public DataBuffer write(byte b) { - return dataBuffer.write(b); + return this.dataBuffer.write(b); } @Override public DataBuffer write(byte[] source) { - return dataBuffer.write(source); + return this.dataBuffer.write(source); } @Override public DataBuffer write(byte[] source, int offset, int length) { - return dataBuffer.write(source, offset, length); + return this.dataBuffer.write(source, offset, length); } @Override public DataBuffer write(DataBuffer... buffers) { - return dataBuffer.write(buffers); + return this.dataBuffer.write(buffers); } @Override public DataBuffer write(ByteBuffer... buffers) { - return dataBuffer.write(buffers); + return this.dataBuffer.write(buffers); } @Override public DataBuffer write(CharSequence charSequence, Charset charset) { - return dataBuffer.write(charSequence, charset); + return this.dataBuffer.write(charSequence, charset); } @Override @Deprecated(since = "6.0") public DataBuffer slice(int index, int length) { - return dataBuffer.slice(index, length); + return this.dataBuffer.slice(index, length); } @Override @Deprecated(since = "6.0") public DataBuffer retainedSlice(int index, int length) { - return dataBuffer.retainedSlice(index, length); + return this.dataBuffer.retainedSlice(index, length); } @Override public DataBuffer split(int index) { - return dataBuffer.split(index); + return this.dataBuffer.split(index); } @Override @Deprecated(since = "6.0") public ByteBuffer asByteBuffer() { - return dataBuffer.asByteBuffer(); + return this.dataBuffer.asByteBuffer(); } @Override @Deprecated(since = "6.0") public ByteBuffer asByteBuffer(int index, int length) { - return dataBuffer.asByteBuffer(index, length); + return this.dataBuffer.asByteBuffer(index, length); } @Override @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer() { - return dataBuffer.toByteBuffer(); + return this.dataBuffer.toByteBuffer(); } @Override @Deprecated(since = "6.0.5") public ByteBuffer toByteBuffer(int index, int length) { - return dataBuffer.toByteBuffer(index, length); + return this.dataBuffer.toByteBuffer(index, length); } @Override public void toByteBuffer(ByteBuffer dest) { - dataBuffer.toByteBuffer(dest); + this.dataBuffer.toByteBuffer(dest); } @Override public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dataBuffer.toByteBuffer(srcPos, dest, destPos, length); + this.dataBuffer.toByteBuffer(srcPos, dest, destPos, length); } @Override public ByteBufferIterator readableByteBuffers() { - return dataBuffer.readableByteBuffers(); + return this.dataBuffer.readableByteBuffers(); } @Override public ByteBufferIterator writableByteBuffers() { - return dataBuffer.writableByteBuffers(); + return this.dataBuffer.writableByteBuffers(); } @Override public InputStream asInputStream() { - return dataBuffer.asInputStream(); + return this.dataBuffer.asInputStream(); } @Override public InputStream asInputStream(boolean releaseOnClose) { - return dataBuffer.asInputStream(releaseOnClose); + return this.dataBuffer.asInputStream(releaseOnClose); } @Override public OutputStream asOutputStream() { - return dataBuffer.asOutputStream(); + return this.dataBuffer.asOutputStream(); } @Override public String toString(Charset charset) { - return dataBuffer.toString(charset); + return this.dataBuffer.toString(charset); } @Override public String toString(int index, int length, Charset charset) { - return dataBuffer.toString(index, length, charset); + return this.dataBuffer.toString(index, length, charset); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 5baa3ac51912..41814c35aeb0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -103,8 +103,7 @@ default SslInfo getSslInfo() { * use such as WebSocket upgrades in the spring-webflux module. */ @Nullable - default T getNativeRequest() - { + default T getNativeRequest() { return null; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 05e92ab8395a..559ff010efa6 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -19,6 +19,8 @@ import java.net.InetSocketAddress; import java.net.URI; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -27,7 +29,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; /** * Wraps another {@link ServerHttpRequest} and delegates all methods to it. @@ -108,9 +109,8 @@ public SslInfo getSslInfo() { } @Override - public T getNativeRequest() - { - return delegate.getNativeRequest(); + public T getNativeRequest() { + return this.delegate.getNativeRequest(); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index eb61473448e1..5ab1708957a7 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -92,8 +92,7 @@ default Integer getRawStatusCode() { * use such as WebSocket upgrades in the spring-webflux module. */ @Nullable - default T getNativeResponse() - { + default T getNativeResponse() { return null; } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index 58f78deb36f1..d2ba47c747ed 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -19,6 +19,8 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; @@ -27,7 +29,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Mono; /** * Wraps another {@link ServerHttpResponse} and delegates all methods to it. @@ -123,8 +124,7 @@ public Mono setComplete() { } @Override - public T getNativeResponse() - { + public T getNativeResponse() { return getDelegate().getNativeResponse(); } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index 703db7254802..e57bd76e8b2c 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -16,7 +16,14 @@ package org.springframework.http.support; -import java.util.*; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -38,6 +45,7 @@ public final class JettyHeadersAdapter implements MultiValueMap { private final HttpFields headers; + @Nullable private final HttpFields.Mutable mutable; @@ -163,7 +171,8 @@ public List put(String key, List value) { case 1 -> { if (oldValues == null) { mutableHttpFields.add(key, value.get(0)); - } else { + } + else { mutableHttpFields.put(key, value.get(0)); } } @@ -178,9 +187,8 @@ public List remove(Object key) { HttpFields.Mutable mutableHttpFields = mutableFields(); List list = null; if (key instanceof String name) { - for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext();) - { - HttpField f = i.next(); + for (ListIterator i = mutableHttpFields.listIterator(); i.hasNext(); ) { + HttpField f = i.next(); if (f.is(name)) { if (list == null) { list = new ArrayList<>(); @@ -222,6 +230,7 @@ public Set>> entrySet() { public Iterator>> iterator() { return new EntryIterator(); } + @Override public int size() { return headers.size(); @@ -230,10 +239,10 @@ public int size() { } private HttpFields.Mutable mutableFields() { - if (mutable == null) { + if (this.mutable == null) { throw new IllegalStateException("Immutable headers"); } - return mutable; + return this.mutable; } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java index d29e711381d2..70f169efb289 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ErrorHandlerIntegrationTests.java @@ -18,7 +18,6 @@ import java.net.URI; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import reactor.core.publisher.Mono; import org.springframework.http.HttpStatus; @@ -28,6 +27,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; import static org.assertj.core.api.Assertions.assertThat; @@ -74,7 +74,8 @@ void handlingError(HttpServer httpServer) throws Exception { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } - @ParameterizedHttpServerTest // SPR-15560 + @ParameterizedHttpServerTest + // SPR-15560 void emptyPathSegments(HttpServer httpServer) throws Exception { startServer(httpServer); @@ -88,7 +89,7 @@ void emptyPathSegments(HttpServer httpServer) throws Exception { // but an application can apply CompactPathRule via RewriteHandler: // https://www.eclipse.org/jetty/documentation/jetty-11/programming_guide.php - HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer + HttpStatus expectedStatus = (httpServer instanceof JettyHttpServer || httpServer instanceof JettyCoreHttpServer ? HttpStatus.BAD_REQUEST : HttpStatus.OK); assertThat(response.getStatusCode()).isEqualTo(expectedStatus); diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java index c4f24f69d18d..97c0c4252c07 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ZeroCopyIntegrationTests.java @@ -19,7 +19,6 @@ import java.io.File; import java.net.URI; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Mono; import org.springframework.core.io.ClassPathResource; @@ -29,6 +28,11 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.web.client.RestTemplate; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -52,7 +56,7 @@ protected HttpHandler createHttpHandler() { @ParameterizedHttpServerTest void zeroCopy(HttpServer httpServer) throws Exception { assumeTrue(httpServer instanceof ReactorHttpServer || httpServer instanceof UndertowHttpServer || httpServer instanceof JettyCoreHttpServer, - "Zero-copy does not support Servlet"); + "Zero-copy does not support Servlet"); startServer(httpServer); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index c40dc6fa5538..a68d384f20d5 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -20,6 +20,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; + import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; /** @@ -31,6 +32,7 @@ public class JettyCoreHttpServer extends AbstractHttpServer { private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove + private Server jettyServer; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index bb22457df381..07e7a1b61ec3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -28,6 +28,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.context.ApplicationContext; import org.springframework.context.i18n.LocaleContext; import org.springframework.core.ResolvableType; @@ -54,8 +57,6 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import org.springframework.web.util.UriUtils; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Default {@link ServerRequest.Builder} implementation. @@ -298,8 +299,7 @@ public Flux getBody() { } @Override - public T getNativeRequest() - { + public T getNativeRequest() { return null; } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index b8f38256495b..a5b2fdceffb7 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.core.OpCode; + import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -55,6 +56,7 @@ public class JettyWebSocketHandlerAdapter { private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); private final WebSocketHandler delegateHandler; + private final Function sessionFactory; @Nullable diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java index f599f9134d65..603d8e618340 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketService.java @@ -27,6 +27,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + import org.springframework.context.Lifecycle; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; @@ -51,7 +53,6 @@ import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; -import reactor.core.publisher.Mono; /** * {@code WebSocketService} implementation that handles a WebSocket HTTP diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java index 66cab235901b..509981c6dced 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyCoreRequestUpgradeStrategy.java @@ -28,6 +28,8 @@ import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; import org.eclipse.jetty.websocket.server.WebSocketCreator; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -41,7 +43,6 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; /** * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 Core. @@ -84,32 +85,31 @@ public Mono upgrade( // Trigger WebFlux preCommit actions before upgrade return exchange.getResponse().setComplete() .then(Mono.deferContextual(contextView -> { - JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( - ContextWebSocketHandler.decorate(handler, contextView), - session -> new JettyWebSocketSession(session, handshakeInfo, factory)); - - WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> - { - if (subProtocol != null) - upgradeResponse.setAcceptedSubProtocol(subProtocol); - return adapter; - }; - - Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); - ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); - try - { - if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) - throw new WebSocketException("request could not be upgraded to websocket"); - } - catch (WebSocketException e) - { - callback.failed(e); - } - - return mono; - })); + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new JettyWebSocketSession(session, handshakeInfo, factory)); + + WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> { + if (subProtocol != null) { + upgradeResponse.setAcceptedSubProtocol(subProtocol); + } + return adapter; + }; + + Callback.Completable callback = new Callback.Completable(); + Mono mono = Mono.fromFuture(callback); + ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); + try { + if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) { + throw new WebSocketException("request could not be upgraded to websocket"); + } + } + catch (WebSocketException ex) { + callback.failed(ex); + } + + return mono; + })); } private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index 82711c47262c..46d8b7090365 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -25,6 +25,8 @@ import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator; import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; import org.eclipse.jetty.websocket.api.Configurable; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; @@ -38,7 +40,6 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; /** * A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 EE10. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java index 91104f2ed8e1..06c01fbfef69 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java @@ -16,11 +16,13 @@ package org.springframework.web.reactive.result.method.annotation; +import java.util.stream.Stream; + import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; - import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,9 +33,13 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; - -import java.util.stream.Stream; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java index a3dbe1a10ca8..83026372da8c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -52,6 +51,13 @@ import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.AbstractHttpHandlerIntegrationTests; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -130,7 +136,8 @@ void sseAsEvent(HttpServer httpServer, ClientHttpConnector connector) throws Exc .uri("/event") .accept(TEXT_EVENT_STREAM) .retrieve() - .bodyToFlux(new ParameterizedTypeReference<>() {}); + .bodyToFlux(new ParameterizedTypeReference<>() { + }); verifyPersonEvents(result); } @@ -143,21 +150,22 @@ void sseAsEventWithoutAcceptHeader(HttpServer httpServer, ClientHttpConnector co .uri("/event") .accept(TEXT_EVENT_STREAM) .retrieve() - .bodyToFlux(new ParameterizedTypeReference<>() {}); + .bodyToFlux(new ParameterizedTypeReference<>() { + }); verifyPersonEvents(result); } private void verifyPersonEvents(Flux> result) { StepVerifier.create(result) - .consumeNextWith( event -> { + .consumeNextWith(event -> { assertThat(event.id()).isEqualTo("0"); assertThat(event.data()).isEqualTo(new Person("foo 0")); assertThat(event.comment()).isEqualTo("bar 0"); assertThat(event.event()).isNull(); assertThat(event.retry()).isNull(); }) - .consumeNextWith( event -> { + .consumeNextWith(event -> { assertThat(event.id()).isEqualTo("1"); assertThat(event.data()).isEqualTo(new Person("foo 1")); assertThat(event.comment()).isEqualTo("bar 1"); @@ -169,7 +177,8 @@ private void verifyPersonEvents(Flux> result) { } @ParameterizedSseTest // SPR-16494 - @Disabled // https://github.com/reactor/reactor-netty/issues/283 + @Disabled + // https://github.com/reactor/reactor-netty/issues/283 void serverDetectsClientDisconnect(HttpServer httpServer, ClientHttpConnector connector) throws Exception { assumeTrue(httpServer instanceof ReactorHttpServer); @@ -297,21 +306,21 @@ public String toString() { static Stream arguments() { return Stream.of( - args(new JettyHttpServer(), new ReactorClientHttpConnector()), - args(new JettyHttpServer(), new JettyClientHttpConnector()), - args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), - args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), - args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), - args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), - args(new ReactorHttpServer(), new ReactorClientHttpConnector()), - args(new ReactorHttpServer(), new JettyClientHttpConnector()), - args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), - args(new TomcatHttpServer(), new ReactorClientHttpConnector()), - args(new TomcatHttpServer(), new JettyClientHttpConnector()), - args(new TomcatHttpServer(), new HttpComponentsClientHttpConnector()), - args(new UndertowHttpServer(), new ReactorClientHttpConnector()), - args(new UndertowHttpServer(), new JettyClientHttpConnector()), - args(new UndertowHttpServer(), new HttpComponentsClientHttpConnector()) + args(new JettyHttpServer(), new ReactorClientHttpConnector()), + args(new JettyHttpServer(), new JettyClientHttpConnector()), + args(new JettyHttpServer(), new HttpComponentsClientHttpConnector()), + args(new JettyCoreHttpServer(), new ReactorClientHttpConnector()), + args(new JettyCoreHttpServer(), new JettyClientHttpConnector()), + args(new JettyCoreHttpServer(), new HttpComponentsClientHttpConnector()), + args(new ReactorHttpServer(), new ReactorClientHttpConnector()), + args(new ReactorHttpServer(), new JettyClientHttpConnector()), + args(new ReactorHttpServer(), new HttpComponentsClientHttpConnector()), + args(new TomcatHttpServer(), new ReactorClientHttpConnector()), + args(new TomcatHttpServer(), new JettyClientHttpConnector()), + args(new TomcatHttpServer(), new HttpComponentsClientHttpConnector()), + args(new UndertowHttpServer(), new ReactorClientHttpConnector()), + args(new UndertowHttpServer(), new JettyClientHttpConnector()), + args(new UndertowHttpServer(), new HttpComponentsClientHttpConnector()) ); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index afa859b6cbf3..c189651e433e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -20,6 +20,9 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; @@ -28,8 +31,6 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Mono; /** * Response that subscribes to the writes source but never posts demand and also @@ -116,8 +117,7 @@ public HttpHeaders getHeaders() { } @Override - public T getNativeResponse() - { + public T getNativeResponse() { return null; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index f991291c13f7..7b176affcf61 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -31,6 +31,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.xnio.OptionMap; +import org.xnio.Xnio; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple3; + import org.springframework.context.ApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -61,11 +67,6 @@ import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; -import org.xnio.OptionMap; -import org.xnio.Xnio; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple3; /** * Base class for reactive WebSocket integration tests. Subclasses must implement From c20b86560ca3382001695a82c90af439cb330eff Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 18:58:19 +0900 Subject: [PATCH 100/146] WIP on fixing leaks --- .../reactive/JettyCoreServerHttpRequest.java | 2 +- .../reactive/JettyRetainedDataBuffer.java | 27 +++++++++++-------- .../bootstrap/JettyCoreHttpServer.java | 14 +++++++--- .../reactive/bootstrap/JettyHttpServer.java | 4 ++- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 02c911e1a58a..d5ea2e553d11 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -110,7 +110,7 @@ public Flux getBody() { } private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory.wrap(chunk.getByteBuffer()), chunk); + return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 77af0e288986..e19a9bf686b2 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -20,9 +20,10 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntPredicate; +import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; import org.springframework.core.io.buffer.DataBuffer; @@ -37,26 +38,30 @@ * @since 6.1.4 */ public class JettyRetainedDataBuffer implements PooledDataBuffer { - private final Retainable retainable; + + private final Content.Chunk chunk; private final DataBuffer dataBuffer; - private final AtomicBoolean allocated = new AtomicBoolean(true); + private final AtomicInteger allocated = new AtomicInteger(1); + - public JettyRetainedDataBuffer(DataBuffer dataBuffer, Retainable retainable) { - this.dataBuffer = dataBuffer; - this.retainable = retainable; - this.retainable.retain(); + public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { + this.chunk = chunk; + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? + this.chunk.retain(); } @Override public boolean isAllocated() { - return this.allocated.get(); + return this.allocated.get() >= 1; } @Override public PooledDataBuffer retain() { - this.retainable.retain(); + if (this.allocated.updateAndGet(c -> c >= 1 ? c + 1 : c) < 1) { + throw new IllegalStateException("released"); + } return this; } @@ -67,8 +72,8 @@ public PooledDataBuffer touch(Object hint) { @Override public boolean release() { - if (this.retainable.release()) { - this.allocated.set(false); + if (this.allocated.decrementAndGet() == 0) { + this.chunk.release(); return true; } return false; diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index a68d384f20d5..003ad2ab6490 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -64,6 +64,7 @@ protected void startInternal() throws Exception { @Override protected void stopInternal() { + boolean wasRunning = this.jettyServer.isRunning(); try { this.jettyServer.stop(); } @@ -72,15 +73,20 @@ protected void stopInternal() { } // TODO remove this or make debug only - this.byteBufferPool.dumpLeaks(); - if (!this.byteBufferPool.getLeaks().isEmpty()) - throw new IllegalStateException("LEAKS"); + if (wasRunning) { + if (!this.byteBufferPool.getLeaks().isEmpty()) { + System.err.println("Leaks:\n" + this.byteBufferPool.dumpLeaks()); + throw new IllegalStateException("LEAKS"); + } + } } @Override protected void resetInternal() { try { - stopInternal(); + if (this.jettyServer.isRunning()) { + stopInternal(); + } this.jettyServer.destroy(); } finally { diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java index 08b7d32b3962..12878528ff1f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyHttpServer.java @@ -79,7 +79,9 @@ protected void stopInternal() throws Exception { @Override protected void resetInternal() { try { - this.jettyServer.stop(); + if (this.jettyServer.isRunning()) { + this.jettyServer.stop(); + } } catch (Exception ex) { throw new IllegalStateException(ex); From a8c9e95085a39e810980bc21946ec60ef7f6e04e Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 19:31:06 +0900 Subject: [PATCH 101/146] WIP on fixing leaks --- .../server/reactive/JettyCoreServerHttpRequest.java | 11 +++++++++-- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d5ea2e553d11..bb597b3341ad 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -34,6 +34,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -106,13 +107,19 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap).doOnNext(this::release); } - private JettyRetainedDataBuffer wrap(Content.Chunk chunk) { + private DataBuffer wrap(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } + private void release(DataBuffer dataBuffer) { + if (dataBuffer instanceof PooledDataBuffer pooled) { + pooled.release(); + } + } + @Override public String getId() { return this.request.getId(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index e19a9bf686b2..a05bf8e4b277 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; +import org.eclipse.jetty.util.BufferUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; From c6e322e07c952f16f179cb21f68b8d541a68ec47 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 31 Jan 2024 22:06:22 +0900 Subject: [PATCH 102/146] WIP on fixing tests --- .../http/server/reactive/JettyRetainedDataBuffer.java | 3 ++- spring-webflux/spring-webflux.gradle | 2 +- .../method/annotation/MultipartWebClientIntegrationTests.java | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index a05bf8e4b277..ac61e231bb91 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -49,7 +49,8 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { this.chunk = chunk; - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? + // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid copy and double slice? this.chunk.retain(); } diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 9fb1d66d6597..5b0452ac0863 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -27,7 +27,7 @@ dependencies { optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } - optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java index cc0fb65e35d7..a6f6d896c153 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MultipartWebClientIntegrationTests.java @@ -169,6 +169,7 @@ void filePartsMono(HttpServer httpServer) throws Exception { @ParameterizedHttpServerTest void transferTo(HttpServer httpServer) throws Exception { // TODO Determine why Undertow fails: https://github.com/spring-projects/spring-framework/issues/25310 + // Jetty is also failing this test in https://github.com/spring-projects/spring-framework/pull/32097 assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails with transferTo"); startServer(httpServer); From e830a029bb0033917bc71ba793c03a7a3af0800f Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 10:32:51 +0900 Subject: [PATCH 103/146] Updated to jetty 12.0.6 --- .../reactive/JettyRetainedDataBuffer.java | 4 +-- .../http/support/JettyHeadersAdapter.java | 25 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index ac61e231bb91..4b3ca53e999c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -49,8 +49,8 @@ public class JettyRetainedDataBuffer implements PooledDataBuffer { public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { this.chunk = chunk; - // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid copy and double slice? + // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); // TODO this copy avoids multipart bugs + this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? this.chunk.retain(); } diff --git a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java index e57bd76e8b2c..83c7b9fff490 100644 --- a/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/support/JettyHeadersAdapter.java @@ -162,21 +162,20 @@ public List get(Object key) { public List put(String key, List value) { HttpFields.Mutable mutableHttpFields = mutableFields(); List oldValues = get(key); - switch (value.size()) { - case 0 -> { - if (oldValues != null) { - mutableHttpFields.remove(key); - } + + if (oldValues == null) { + switch (value.size()) { + case 0 -> {} + case 1 -> mutableHttpFields.add(key, value.get(0)); + default -> mutableHttpFields.add(key, value); } - case 1 -> { - if (oldValues == null) { - mutableHttpFields.add(key, value.get(0)); - } - else { - mutableHttpFields.put(key, value.get(0)); - } + } + else { + switch (value.size()) { + case 0 -> mutableHttpFields.remove(key); + case 1 -> mutableHttpFields.put(key, value.get(0)); + default -> mutableHttpFields.put(key, value); } - default -> mutableHttpFields.put(key, value); } return oldValues; } From 14d9a769f8c9d1f59e6553a5ef64ef9dc41e3b69 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 11:30:33 +0900 Subject: [PATCH 104/146] Move all response cookies to multiMap for possible mutation by spring --- .../reactive/JettyCoreServerHttpResponse.java | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 09261ef76c52..5d533cf196a5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -24,11 +24,13 @@ import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; @@ -131,7 +133,7 @@ public Mono writeWith(Path file, long position, long count) { mono = Mono.fromFuture(callback); try { // TODO: Why does intellij warn about possible blocking call? - // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel + // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel? SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } @@ -159,7 +161,6 @@ private Mono ensureCommitted() { private void writeCookies() { if (this.cookies != null) { - // TODO: are we doubling up on cookies already existing in response? this.cookies.values().stream() .flatMap(List::stream) .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); @@ -227,18 +228,12 @@ public boolean setRawStatusCode(@Nullable Integer value) { @Override public MultiValueMap getCookies() { - if (this.cookies == null) { - initializeCookies(); - } - return this.cookies; + return initializeCookies(); } @Override public void addCookie(ResponseCookie cookie) { - if (this.cookies == null) { - initializeCookies(); - } - this.cookies.add(cookie.getName(), cookie); + initializeCookies().add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -247,13 +242,27 @@ public T getNativeResponse() { return (T) this.response; } - private void initializeCookies() { - this.cookies = new LinkedMultiValueMap<>(); - for (HttpField f : this.response.getHeaders()) { - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField && setCookieHttpField.getHttpCookie() instanceof HttpResponseCookie httpResponseCookie) { - this.cookies.add(httpResponseCookie.getName(), httpResponseCookie.getResponseCookie()); + private LinkedMultiValueMap initializeCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap<>(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.cookies.add(responseCookie.getName(), responseCookie); + i.remove(); + } } } + return this.cookies; } private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { From d5ff3656fb084275c5002980b6302aed4f96c0c8 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 12:03:35 +0900 Subject: [PATCH 105/146] Use an atomicReference to release the last used databuffer after onNext has returned. --- .../server/reactive/JettyCoreServerHttpRequest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index bb597b3341ad..d1e5a7f99bbb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -107,14 +108,20 @@ public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be // retained within a call to onNext. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::wrap).doOnNext(this::release); + + // TODO find a better way to release after each onNext call to a subscriber + AtomicReference last = new AtomicReference<>(); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) + .map(this::wrap) + .doOnNext(db -> release(last.getAndSet(db))) + .doOnComplete(() -> release(last.getAndSet(null))); } private DataBuffer wrap(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } - private void release(DataBuffer dataBuffer) { + private static void release(DataBuffer dataBuffer) { if (dataBuffer instanceof PooledDataBuffer pooled) { pooled.release(); } From 8efbc37c8c163379f07b23a2ac5939ae14614fa7 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 1 Feb 2024 12:20:18 +0900 Subject: [PATCH 106/146] less allocations for releasing after onNext --- .../reactive/JettyCoreServerHttpRequest.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d1e5a7f99bbb..bf1f537a1d2b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -110,23 +110,31 @@ public Flux getBody() { // retained within a call to onNext. // TODO find a better way to release after each onNext call to a subscriber - AtomicReference last = new AtomicReference<>(); + DataBufferReleaser releaser = new DataBufferReleaser(); return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) .map(this::wrap) - .doOnNext(db -> release(last.getAndSet(db))) - .doOnComplete(() -> release(last.getAndSet(null))); + .doOnNext(releaser::onNext) + .doOnComplete(releaser::onComplete); } - private DataBuffer wrap(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); - } + private static class DataBufferReleaser { + private final AtomicReference last = new AtomicReference<>(); + + public void onNext(@Nullable DataBuffer dataBuffer) { + if (last.getAndSet(dataBuffer) instanceof PooledDataBuffer pooled) { + pooled.release(); + } + } - private static void release(DataBuffer dataBuffer) { - if (dataBuffer instanceof PooledDataBuffer pooled) { - pooled.release(); + public void onComplete() { + onNext(null); } } + private DataBuffer wrap(Content.Chunk chunk) { + return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); + } + @Override public String getId() { return this.request.getId(); From 9a68ff7612faa2f86a7660299e1f1adcabd8617a Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 1 Feb 2024 19:09:31 +1100 Subject: [PATCH 107/146] release DataBuffer from the JettyCoreServerHttpResponse Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpRequest.java | 29 ++----------------- .../reactive/JettyCoreServerHttpResponse.java | 8 +++-- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index bf1f537a1d2b..51fb3989d16a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import org.eclipse.jetty.http.HttpFields; @@ -31,11 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; -import reactor.core.publisher.Flux; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -46,6 +42,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; @@ -106,29 +103,9 @@ public URI getURI() { @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. The chunks are converted to RetainedDataBuffers with wrapping and can be - // retained within a call to onNext. - - // TODO find a better way to release after each onNext call to a subscriber - DataBufferReleaser releaser = new DataBufferReleaser(); + // then wrapped as a Flux. return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) - .map(this::wrap) - .doOnNext(releaser::onNext) - .doOnComplete(releaser::onComplete); - } - - private static class DataBufferReleaser { - private final AtomicReference last = new AtomicReference<>(); - - public void onNext(@Nullable DataBuffer dataBuffer) { - if (last.getAndSet(dataBuffer) instanceof PooledDataBuffer pooled) { - pooled.release(); - } - } - - public void onComplete() { - onNext(null); - } + .map(this::wrap); } private DataBuffer wrap(Content.Chunk chunk) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 5d533cf196a5..fb68fc878ca8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,11 +42,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -56,6 +54,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -189,12 +189,14 @@ protected Action process() { @Override protected void onCompleteSuccess() { byteBufferIterator.close(); + DataBufferUtils.release(dataBuffer); callback.complete(null); } @Override protected void onCompleteFailure(Throwable cause) { byteBufferIterator.close(); + DataBufferUtils.release(dataBuffer); callback.failed(cause); } }.iterate(); From 011d85687ed1be15cdc9daeab2ca691c8fb44423 Mon Sep 17 00:00:00 2001 From: gregw Date: Sat, 3 Feb 2024 09:03:02 +0100 Subject: [PATCH 108/146] fixed style --- .../http/server/reactive/JettyCoreServerHttpRequest.java | 3 ++- .../http/server/reactive/JettyCoreServerHttpResponse.java | 5 +++-- .../http/server/reactive/JettyRetainedDataBuffer.java | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 51fb3989d16a..d7c073d1496d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -30,6 +30,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; +import reactor.core.publisher.Flux; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; @@ -42,7 +44,6 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import reactor.core.publisher.Flux; import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index fb68fc878ca8..54df884063eb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -42,6 +42,9 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; @@ -54,8 +57,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java index 4b3ca53e999c..b35fabf017c3 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java @@ -25,7 +25,6 @@ import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Retainable; -import org.eclipse.jetty.util.BufferUtil; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; From 5e71a1cefb14be0cc007d3717013ddc16e3fb11b Mon Sep 17 00:00:00 2001 From: Olivier Lamy Date: Tue, 6 Feb 2024 10:40:52 +1000 Subject: [PATCH 109/146] fix build with jetty 12.0.7-SNAPSHOT Signed-off-by: Olivier Lamy --- build.gradle | 1 + framework-api/framework-api.gradle | 1 + spring-jcl/spring-jcl.gradle | 2 ++ 3 files changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index fe60cafa1bb9..acf15bc9e22d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" group = "org.springframework" repositories { + mavenLocal() mavenCentral() maven { url "https://repo.spring.io/milestone" diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index 016ca58a7a40..5cbe94dea6c2 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -8,6 +8,7 @@ description = "Spring Framework API Docs" apply from: "${rootDir}/gradle/publications.gradle" repositories { + mavenLocal() maven { url "https://repo.spring.io/release" } diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle index d609737b2551..560d85324cb0 100644 --- a/spring-jcl/spring-jcl.gradle +++ b/spring-jcl/spring-jcl.gradle @@ -3,4 +3,6 @@ description = "Spring Commons Logging Bridge" dependencies { optional("org.apache.logging.log4j:log4j-api") optional("org.slf4j:slf4j-api") + optional("biz.aQute.bnd:biz.aQute.bnd.annotation:6.3.1") + optional("org.osgi:osgi.annotation:8.1.0") } From 7cb17cef55e59be3539095aa7e4e1d708df031e5 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Mar 2024 16:47:29 +0100 Subject: [PATCH 110/146] updated to jetty 12.0.7 --- .../reactive/bootstrap/JettyCoreHttpServer.java | 16 ++++++++++------ .../MultipartRouterFunctionIntegrationTests.java | 6 ++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index 003ad2ab6490..ba1a085ebcec 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -16,6 +16,8 @@ package org.springframework.web.testfixture.http.server.reactive.bootstrap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -31,14 +33,16 @@ */ public class JettyCoreHttpServer extends AbstractHttpServer { - private ArrayByteBufferPool.Tracking byteBufferPool; // TODO remove + protected Log logger = LogFactory.getLog(getClass().getName()); - private Server jettyServer; + private ArrayByteBufferPool byteBufferPool; + private Server jettyServer; @Override protected void initServer() { - this.byteBufferPool = new ArrayByteBufferPool.Tracking(); + if (logger.isTraceEnabled()) + this.byteBufferPool = new ArrayByteBufferPool.Tracking(); this.jettyServer = new Server(null, null, byteBufferPool); ServerConnector connector = new ServerConnector(this.jettyServer); @@ -73,9 +77,9 @@ protected void stopInternal() { } // TODO remove this or make debug only - if (wasRunning) { - if (!this.byteBufferPool.getLeaks().isEmpty()) { - System.err.println("Leaks:\n" + this.byteBufferPool.dumpLeaks()); + if (wasRunning && this.byteBufferPool instanceof ArrayByteBufferPool.Tracking tracking) { + if (!tracking.getLeaks().isEmpty()) { + System.err.println("Leaks:\n" + tracking.dumpLeaks()); throw new IllegalStateException("LEAKS"); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index 5388b7566f97..dff5f1fc2863 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -49,11 +49,13 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; +import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** @@ -165,6 +167,10 @@ void proxy(HttpServer httpServer) throws Exception { assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow currently fails proxying requests"); startServer(httpServer); + // TODO For JettyCore this test passes, but calls demand on the request Flux after the handling cycle is + // complete, causing an exception to be logged; and leaks buffers that appear not be released by the + // test application. + Mono> result = webClient .post() .uri("http://localhost:" + this.port + "/proxy") From f47baaf7a15307b3c25e5ed1578fdd6fb85c0d95 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 7 Mar 2024 16:52:56 +0100 Subject: [PATCH 111/146] updated to jetty 12.0.7 --- build.gradle | 1 - framework-api/framework-api.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index acf15bc9e22d..fe60cafa1bb9 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ configure(allprojects) { project -> apply plugin: "org.springframework.build.localdev" group = "org.springframework" repositories { - mavenLocal() mavenCentral() maven { url "https://repo.spring.io/milestone" diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index 5cbe94dea6c2..016ca58a7a40 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -8,7 +8,6 @@ description = "Spring Framework API Docs" apply from: "${rootDir}/gradle/publications.gradle" repositories { - mavenLocal() maven { url "https://repo.spring.io/release" } From 9bba36bdba7f218c02aa485541886bcfc63c6244 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 22 Mar 2024 11:13:23 +0100 Subject: [PATCH 112/146] fixed checkstyle --- .../function/MultipartRouterFunctionIntegrationTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index dff5f1fc2863..a0bf978c86f0 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -49,13 +49,11 @@ import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; -import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyCoreHttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.UndertowHttpServer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.springframework.web.reactive.function.server.RouterFunctions.route; /** From f06131b234f739e14800b7faee8358ec98ed3a29 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:10:57 +1100 Subject: [PATCH 113/146] Implement a JettyWebSocketSession based on demand. Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpRequest.java | 1 - .../reactive/JettyCoreServerHttpResponse.java | 4 +- spring-webflux/spring-webflux.gradle | 1 + .../adapter/JettyWebSocketHandlerAdapter.java | 103 ++++++------ .../socket/adapter/JettyWebSocketSession.java | 150 ++++++++++++------ 5 files changed, 148 insertions(+), 111 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index d7c073d1496d..59bf4d0d28c0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -53,7 +53,6 @@ * @author Greg Wilkins * @since 6.2 */ -// TODO: extend AbstractServerHttpRequest for websocket. class JettyCoreServerHttpRequest implements ServerHttpRequest { private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 54df884063eb..ced7f42c7ddd 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -65,7 +65,6 @@ * @author Lachlan Roberts * @since 6.2 */ -// TODO: extend AbstractServerHttpResponse for websocket. class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { private final AtomicBoolean committed = new AtomicBoolean(false); @@ -133,8 +132,7 @@ public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { - // TODO: Why does intellij warn about possible blocking call? - // Because it can block and we want to be fully asynchronous. Use AsynchronousFileChannel? + @SuppressWarnings("BlockingMethodInNonBlockingContext") SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); } diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle index 5b0452ac0863..93c7ee5b5292 100644 --- a/spring-webflux/spring-webflux.gradle +++ b/spring-webflux/spring-webflux.gradle @@ -28,6 +28,7 @@ dependencies { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-server") + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-client") optional("org.freemarker:freemarker") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index a5b2fdceffb7..9e99f3259f26 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -19,24 +19,20 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.function.Function; import java.util.function.IntPredicate; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.core.OpCode; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -51,15 +47,12 @@ * @author Rossen Stoyanchev * @since 5.0 */ -@WebSocket -public class JettyWebSocketHandlerAdapter { - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - +public class JettyWebSocketHandlerAdapter implements Session.Listener { private final WebSocketHandler delegateHandler; private final Function sessionFactory; - @Nullable + @SuppressWarnings("NotNullFieldNotInitialized") private JettyWebSocketSession delegateSession; public JettyWebSocketHandlerAdapter(WebSocketHandler handler, @@ -71,65 +64,63 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler handler, this.sessionFactory = sessionFactory; } - @OnWebSocketOpen + @Override public void onWebSocketOpen(Session session) { - this.delegateSession = this.sessionFactory.apply(session); + this.delegateSession = Objects.requireNonNull(this.sessionFactory.apply(session)); this.delegateHandler.handle(this.delegateSession) - .checkpoint(session.getUpgradeRequest().getRequestURI() + " [JettyWebSocketHandlerAdapter]") - .subscribe(this.delegateSession); + .subscribe(new Subscriber<>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void unused) { + } + + @Override + public void onError(Throwable t) { + delegateSession.onHandlerError(t); + } + + @Override + public void onComplete() { + delegateSession.onHandleComplete(); + } + }); } - @OnWebSocketMessage + @Override public void onWebSocketText(String message) { - if (this.delegateSession != null) { - byte[] bytes = message.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - } + byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketMessage + @Override public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { - if (this.delegateSession != null) { - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); - buffer = new JettyDataBuffer(buffer, callback); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - } - else { - callback.succeed(); - } + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); + buffer = new JettyDataBuffer(buffer, callback); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketFrame - public void onWebSocketFrame(Frame frame, Callback callback) { - if (this.delegateSession != null) { - if (OpCode.PONG == frame.getOpCode()) { - ByteBuffer byteBuffer = (frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD); - DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); - buffer = new JettyDataBuffer(buffer, callback); - WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); - return; - } - } - - callback.succeed(); + @Override + public void onWebSocketPong(ByteBuffer payload) { + DataBuffer buffer = this.delegateSession.bufferFactory().wrap(BufferUtil.copy(payload)); + WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); + this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); } - @OnWebSocketClose + @Override public void onWebSocketClose(int statusCode, String reason) { - if (this.delegateSession != null) { - this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); - } + this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); } - @OnWebSocketError + @Override public void onWebSocketError(Throwable cause) { - if (this.delegateSession != null) { - this.delegateSession.handleError(cause); - } + this.delegateSession.handleError(cause); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index d6adca7e9298..c85a4fe523b0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -16,12 +16,15 @@ package org.springframework.web.reactive.socket.adapter; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicLong; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -36,13 +39,23 @@ /** * Spring {@link WebSocketSession} implementation that adapts to a Jetty - * WebSocket {@link org.eclipse.jetty.websocket.api.Session}. + * WebSocket {@link Session}. * * @author Violeta Georgieva * @author Rossen Stoyanchev * @since 5.0 */ -public class JettyWebSocketSession extends AbstractListenerWebSocketSession { +public class JettyWebSocketSession extends AbstractWebSocketSession { + + private final Flux flux; + private final AtomicLong requested = new AtomicLong(0); + + private final Sinks.One closeStatusSink = Sinks.one(); + @Nullable + private FluxSink sink; + + @Nullable + private final Sinks.Empty handlerCompletionSink; public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) { this(session, info, factory, null); @@ -51,52 +64,45 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory, @Nullable Sinks.Empty completionSink) { - super(session, ObjectUtils.getIdentityHexString(session), info, factory, completionSink); - // TODO: suspend causes failures if invoked at this stage - // suspendReceiving(); + super(session, ObjectUtils.getIdentityHexString(session), info, factory); + this.handlerCompletionSink = completionSink; + this.flux = Flux.create(emitter -> { + this.sink = emitter; + emitter.onRequest(n -> + { + requested.addAndGet(n); + tryDemand(); + }); + }); } - - @Override - protected boolean canSuspendReceiving() { - // Jetty 12 TODO: research suspend functionality in Jetty 12 - return false; + void handleMessage(WebSocketMessage.Type type, WebSocketMessage message) { + this.sink.next(message); + tryDemand(); } - @Override - protected void suspendReceiving() { + void handleError(Throwable ex) { } - @Override - protected void resumeReceiving() { + void handleClose(CloseStatus closeStatus) { + this.closeStatusSink.tryEmitValue(closeStatus); + this.sink.complete(); } - @Override - protected boolean sendMessage(WebSocketMessage message) throws IOException { - DataBuffer dataBuffer = message.getPayload(); - Session session = getDelegate(); - if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - getSendProcessor().setReadyToSend(false); - String text = dataBuffer.toString(StandardCharsets.UTF_8); - session.sendText(text, new SendProcessorCallback()); + void onHandlerError(Throwable ex) { + if (this.handlerCompletionSink != null) { + // Ignore result: can't overflow, ok if not first or no one listens + this.handlerCompletionSink.tryEmitError(ex); } - else { - if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - getSendProcessor().setReadyToSend(false); - } - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - switch (message.getType()) { - case BINARY -> session.sendBinary(byteBuffer, new SendProcessorCallback()); - case PING -> session.sendPing(byteBuffer, new SendProcessorCallback()); - case PONG -> session.sendPong(byteBuffer, new SendProcessorCallback()); - default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); - } - } - } + close(CloseStatus.SERVER_ERROR); + } + + void onHandleComplete() { + if (this.handlerCompletionSink != null) { + // Ignore result: can't overflow, ok if not first or no one listens + this.handlerCompletionSink.tryEmitEmpty(); } - return true; + close(); } @Override @@ -108,25 +114,67 @@ public boolean isOpen() { public Mono close(CloseStatus status) { Callback.Completable callback = new Callback.Completable(); getDelegate().close(status.getCode(), status.getReason(), callback); - return Mono.fromFuture(callback); } + @Override + public Mono closeStatus() { + return closeStatusSink.asMono(); + } - private final class SendProcessorCallback implements Callback { - - @Override - public void fail(Throwable x) { - getSendProcessor().cancel(); - getSendProcessor().onError(x); - } + @Override + public Flux receive() { + return flux; + } - @Override - public void succeed() { - getSendProcessor().setReadyToSend(true); - getSendProcessor().onWritePossible(); + private void tryDemand() + { + while (true) + { + long r = requested.get(); + if (r == 0) + return; + + // TODO: protect against readpending from multiple demand. + if (requested.compareAndSet(r, r - 1)) + { + getDelegate().demand(); + return; + } } + } + @Override + public Mono send(Publisher messages) { + return Flux.from(messages) + .flatMap(this::sendMessage, 1) + .then(); } + protected Mono sendMessage(WebSocketMessage message) { + + Callback.Completable completable = new Callback.Completable(); + + DataBuffer dataBuffer = message.getPayload(); + Session session = getDelegate(); + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + String text = dataBuffer.toString(StandardCharsets.UTF_8); + session.sendText(text, completable); + } + else { + // TODO: Ping and Pong message should combine payload into single buffer? + try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + switch (message.getType()) { + case BINARY -> session.sendBinary(byteBuffer, completable); + case PING -> session.sendPing(byteBuffer, completable); + case PONG -> session.sendPong(byteBuffer, completable); + default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + } + } + } + return Mono.fromFuture(completable); + } } From 8cc8b11f49f3336c27041eeb97d9cebd81c5cc3f Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:11:45 +1100 Subject: [PATCH 114/146] Add a Jetty implementation of the spring WebSocketClient interface. Signed-off-by: Lachlan Roberts --- .../socket/client/JettyWebSocketClient.java | 78 +++++++++++++++++++ ...ractReactiveWebSocketIntegrationTests.java | 2 + 2 files changed, 80 insertions(+) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java new file mode 100644 index 000000000000..9c784d99c266 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -0,0 +1,78 @@ +package org.springframework.web.reactive.socket.client; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.JettyUpgradeListener; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; + +public class JettyWebSocketClient implements WebSocketClient { + + private final org.eclipse.jetty.websocket.client.WebSocketClient client; + + public JettyWebSocketClient() + { + this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); + LifeCycle.start(this.client); + } + + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) + { + this.client = client; + } + + @Override + public Mono execute(URI url, WebSocketHandler handler) { + return execute(url, null, handler); + } + + @Override + public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandler handler) { + + ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); + upgradeRequest.setSubProtocols(handler.getSubProtocols()); + if (headers != null) + headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); + + AtomicReference handshakeInfo = new AtomicReference<>(); + JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { + @Override + public void onHandshakeResponse(Request request, Response response) { + String protocol = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); + HttpHeaders responseHeaders = new HttpHeaders(); + response.getHeaders().forEach(header -> responseHeaders.addAll(header.getName(), header.getValueList())); + handshakeInfo.set(new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol)); + } + }; + + Sinks.Empty completion = Sinks.empty(); + JettyWebSocketHandlerAdapter handlerAdapter = new JettyWebSocketHandlerAdapter(handler, session -> + new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); + try { + this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) + .whenComplete((session, throwable) -> { + if (throwable != null) + completion.tryEmitError(throwable); + }); + return completion.asMono(); + } + catch (IOException e) { + return Mono.error(e); + } + } +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 7b176affcf61..9392f0cca98e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -45,6 +45,7 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.socket.client.JettyWebSocketClient; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.TomcatWebSocketClient; import org.springframework.web.reactive.socket.client.UndertowWebSocketClient; @@ -92,6 +93,7 @@ static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), + new JettyWebSocketClient(), new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From 2b9510d296a6e319b648dd69a22c1bda285fbd07 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 6 Feb 2024 21:44:40 +1100 Subject: [PATCH 115/146] Prevent possible ReadPendingException from multiple demand. Signed-off-by: Lachlan Roberts --- .../adapter/JettyWebSocketHandlerAdapter.java | 6 +- .../socket/adapter/JettyWebSocketSession.java | 103 ++++++++++++------ 2 files changed, 72 insertions(+), 37 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 9e99f3259f26..33ad751a130a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -95,7 +95,7 @@ public void onWebSocketText(String message) { byte[] bytes = message.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override @@ -103,14 +103,14 @@ public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); buffer = new JettyDataBuffer(buffer, callback); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override public void onWebSocketPong(ByteBuffer payload) { DataBuffer buffer = this.delegateSession.bufferFactory().wrap(BufferUtil.copy(payload)); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); - this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage); + this.delegateSession.handleMessage(webSocketMessage); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index c85a4fe523b0..ba3d0b46182b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -18,8 +18,10 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.reactivestreams.Publisher; @@ -48,10 +50,12 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; - private final AtomicLong requested = new AtomicLong(0); - private final Sinks.One closeStatusSink = Sinks.one(); - @Nullable + private final Lock lock = new ReentrantLock(); + private long requested = 0; + private boolean awaitingDemand = false; + + @SuppressWarnings("NotNullFieldNotInitialized") private FluxSink sink; @Nullable @@ -70,15 +74,49 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.sink = emitter; emitter.onRequest(n -> { - requested.addAndGet(n); - tryDemand(); + boolean demand = false; + lock.lock(); + try + { + requested += n; + if (!awaitingDemand && requested > 0) { + requested--; + awaitingDemand = true; + demand = true; + } + } + finally { + lock.unlock(); + } + + if (demand) + getDelegate().demand(); }); }); } - void handleMessage(WebSocketMessage.Type type, WebSocketMessage message) { + void handleMessage(WebSocketMessage message) { this.sink.next(message); - tryDemand(); + + boolean demand = false; + lock.lock(); + try + { + if (!awaitingDemand) + throw new IllegalStateException(); + awaitingDemand = false; + if (requested > 0) { + requested--; + awaitingDemand = true; + demand = true; + } + } + finally { + lock.unlock(); + } + + if (demand) + getDelegate().demand(); } void handleError(Throwable ex) { @@ -127,23 +165,6 @@ public Flux receive() { return flux; } - private void tryDemand() - { - while (true) - { - long r = requested.get(); - if (r == 0) - return; - - // TODO: protect against readpending from multiple demand. - if (requested.compareAndSet(r, r - 1)) - { - getDelegate().demand(); - return; - } - } - } - @Override public Mono send(Publisher messages) { return Flux.from(messages) @@ -162,17 +183,31 @@ protected Mono sendMessage(WebSocketMessage message) { session.sendText(text, completable); } else { - // TODO: Ping and Pong message should combine payload into single buffer? - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - switch (message.getType()) { - case BINARY -> session.sendBinary(byteBuffer, completable); - case PING -> session.sendPing(byteBuffer, completable); - case PONG -> session.sendPong(byteBuffer, completable); - default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + switch (message.getType()) { + case BINARY -> + { + try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + session.sendBinary(byteBuffer, completable); + } } } + case PING -> + { + // Maximum size of Control frame payload is 125, per RFC 6455. + ByteBuffer buffer = BufferUtil.allocate(125); + dataBuffer.toByteBuffer(buffer); + session.sendPing(buffer, completable); + } + case PONG -> + { + // Maximum size of Control frame payload is 125, per RFC 6455. + ByteBuffer buffer = BufferUtil.allocate(125); + dataBuffer.toByteBuffer(buffer); + session.sendPong(buffer, completable); + } + default -> throw new IllegalArgumentException("Unexpected message type: " + message.getType()); } } return Mono.fromFuture(completable); From 96a8e99d276cac6915fbcad7ea5ef6654648d7f9 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 7 Feb 2024 00:23:20 +1100 Subject: [PATCH 116/146] Update the JettyWebSocketHandlerAdapter as a Session.Listener Signed-off-by: Lachlan Roberts --- .../jetty/JettyWebSocketHandlerAdapter.java | 91 +++++++++---------- .../jetty/JettyRequestUpgradeStrategy.java | 2 +- 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index b996b920e1c3..c4576b31f43f 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -20,16 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; -import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.core.OpCode; import org.springframework.util.Assert; import org.springframework.web.socket.BinaryMessage; @@ -45,17 +38,13 @@ * @author Rossen Stoyanchev * @since 4.0 */ -@WebSocket -public class JettyWebSocketHandlerAdapter { - - private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - +public class JettyWebSocketHandlerAdapter implements Session.Listener { private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class); - private final WebSocketHandler webSocketHandler; - private final JettyWebSocketSession wsSession; + @SuppressWarnings("NotNullFieldNotInitialized") + private Session nativeSession; public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { @@ -66,68 +55,57 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebS } - @OnWebSocketOpen + @Override public void onWebSocketOpen(Session session) { try { + this.nativeSession = session; this.wsSession.initializeNativeSession(session); this.webSocketHandler.afterConnectionEstablished(this.wsSession); + this.nativeSession.demand(); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketMessage + @Override public void onWebSocketText(String payload) { TextMessage message = new TextMessage(payload); try { this.webSocketHandler.handleMessage(this.wsSession, message); + this.nativeSession.demand(); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketMessage + @Override public void onWebSocketBinary(ByteBuffer payload, Callback callback) { - BinaryMessage message = new BinaryMessage(copyByteBuffer(payload), true); + BinaryMessage message = new BinaryMessage(BufferUtil.copy(payload), true); + callback.succeed(); try { this.webSocketHandler.handleMessage(this.wsSession, message); - callback.succeed(); + this.nativeSession.demand(); } catch (Exception ex) { - callback.fail(ex); - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + tryCloseWithError(ex); } } - @OnWebSocketFrame - public void onWebSocketFrame(Frame frame, Callback callback) { - if (OpCode.PONG == frame.getOpCode()) { - ByteBuffer payload = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; - PongMessage message = new PongMessage(copyByteBuffer(payload)); - try { - this.webSocketHandler.handleMessage(this.wsSession, message); - callback.succeed(); - } - catch (Exception ex) { - callback.fail(ex); - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); - } + @Override + public void onWebSocketPong(ByteBuffer payload) { + PongMessage message = new PongMessage(BufferUtil.copy(payload)); + try { + this.webSocketHandler.handleMessage(this.wsSession, message); + this.nativeSession.demand(); } - else { - callback.succeed(); + catch (Exception ex) { + tryCloseWithError(ex); } } - private static ByteBuffer copyByteBuffer(ByteBuffer src) { - ByteBuffer dest = ByteBuffer.allocate(src.remaining()); - dest.put(src); - dest.flip(); - return dest; - } - - @OnWebSocketClose + @Override public void onWebSocketClose(int statusCode, String reason) { CloseStatus closeStatus = new CloseStatus(statusCode, reason); try { @@ -135,18 +113,31 @@ public void onWebSocketClose(int statusCode, String reason) { } catch (Exception ex) { if (logger.isWarnEnabled()) { - logger.warn("Unhandled exception after connection closed for " + this, ex); + logger.warn("Unhandled exception from afterConnectionClosed for " + this, ex); } } } - @OnWebSocketError + @Override public void onWebSocketError(Throwable cause) { try { this.webSocketHandler.handleTransportError(this.wsSession, cause); } catch (Exception ex) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, ex, logger); + if (logger.isWarnEnabled()) { + logger.warn("Unhandled exception from handleTransportError for " + this, ex); + } + } + } + + private void tryCloseWithError(Throwable t) { + + if (nativeSession.isOpen()) { + ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); + } + else { + // Session might be O-SHUT waiting for response close frame, so abort to close the connection. + nativeSession.disconnect(); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java index 026f9af4fd73..2ed4542e3111 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/jetty/JettyRequestUpgradeStrategy.java @@ -45,7 +45,7 @@ import org.springframework.web.socket.server.RequestUpgradeStrategy; /** - * A {@link RequestUpgradeStrategy} for Jetty 11. + * A {@link RequestUpgradeStrategy} for Jetty 12 EE10. * * @author Rossen Stoyanchev * @since 5.3.4 From f195e4edbc552dec80b408f31af18fbc8771bcaf Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 7 Feb 2024 01:07:38 +1100 Subject: [PATCH 117/146] fixes for checkstyle Signed-off-by: Lachlan Roberts --- .../adapter/JettyWebSocketHandlerAdapter.java | 3 +- .../socket/adapter/JettyWebSocketSession.java | 55 +++++++++---------- .../socket/client/JettyWebSocketClient.java | 32 ++++++++--- .../jetty/JettyWebSocketHandlerAdapter.java | 6 +- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 33ad751a130a..6f42e85d7ec2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -26,7 +26,6 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -40,7 +39,7 @@ import org.springframework.web.reactive.socket.WebSocketMessage.Type; /** - * Jetty {@link WebSocket @WebSocket} handler that delegates events to a + * Jetty {@link org.eclipse.jetty.websocket.api.Session.Listener} handler that delegates events to a * reactive {@link WebSocketHandler} and its session. * * @author Violeta Georgieva diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index ba3d0b46182b..2f1ff6728e2f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -72,25 +72,24 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.handlerCompletionSink = completionSink; this.flux = Flux.create(emitter -> { this.sink = emitter; - emitter.onRequest(n -> - { + emitter.onRequest(n -> { boolean demand = false; - lock.lock(); - try - { - requested += n; - if (!awaitingDemand && requested > 0) { - requested--; - awaitingDemand = true; + this.lock.lock(); + try { + this.requested += n; + if (!this.awaitingDemand && this.requested > 0) { + this.requested--; + this.awaitingDemand = true; demand = true; } } finally { - lock.unlock(); + this.lock.unlock(); } - if (demand) + if (demand) { getDelegate().demand(); + } }); }); } @@ -99,24 +98,25 @@ void handleMessage(WebSocketMessage message) { this.sink.next(message); boolean demand = false; - lock.lock(); - try - { - if (!awaitingDemand) + this.lock.lock(); + try { + if (!this.awaitingDemand) { throw new IllegalStateException(); - awaitingDemand = false; - if (requested > 0) { - requested--; - awaitingDemand = true; + } + this.awaitingDemand = false; + if (this.requested > 0) { + this.requested--; + this.awaitingDemand = true; demand = true; } } finally { - lock.unlock(); + this.lock.unlock(); } - if (demand) + if (demand) { getDelegate().demand(); + } } void handleError(Throwable ex) { @@ -157,12 +157,12 @@ public Mono close(CloseStatus status) { @Override public Mono closeStatus() { - return closeStatusSink.asMono(); + return this.closeStatusSink.asMono(); } @Override public Flux receive() { - return flux; + return this.flux; } @Override @@ -184,8 +184,7 @@ protected Mono sendMessage(WebSocketMessage message) { } else { switch (message.getType()) { - case BINARY -> - { + case BINARY -> { try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { while (iterator.hasNext()) { ByteBuffer byteBuffer = iterator.next(); @@ -193,15 +192,13 @@ protected Mono sendMessage(WebSocketMessage message) { } } } - case PING -> - { + case PING -> { // Maximum size of Control frame payload is 125, per RFC 6455. ByteBuffer buffer = BufferUtil.allocate(125); dataBuffer.toByteBuffer(buffer); session.sendPing(buffer, completable); } - case PONG -> - { + case PONG -> { // Maximum size of Control frame payload is 125, per RFC 6455. ByteBuffer buffer = BufferUtil.allocate(125); dataBuffer.toByteBuffer(buffer); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 9c784d99c266..c47e5fc8d4d3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.reactive.socket.client; import java.io.IOException; @@ -25,14 +41,12 @@ public class JettyWebSocketClient implements WebSocketClient { private final org.eclipse.jetty.websocket.client.WebSocketClient client; - public JettyWebSocketClient() - { + public JettyWebSocketClient() { this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); LifeCycle.start(this.client); } - public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) - { + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { this.client = client; } @@ -46,8 +60,9 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); upgradeRequest.setSubProtocols(handler.getSubProtocols()); - if (headers != null) + if (headers != null) { headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); + } AtomicReference handshakeInfo = new AtomicReference<>(); JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { @@ -66,13 +81,14 @@ public void onHandshakeResponse(Request request, Response response) { try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) .whenComplete((session, throwable) -> { - if (throwable != null) + if (throwable != null) { completion.tryEmitError(throwable); + } }); return completion.asMono(); } - catch (IOException e) { - return Mono.error(e); + catch (IOException ex) { + return Mono.error(ex); } } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index c4576b31f43f..959ab677f1be 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -33,7 +33,7 @@ import org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator; /** - * Adapts {@link WebSocketHandler} to the Jetty WebSocket API. + * Adapts {@link WebSocketHandler} to the Jetty WebSocket API {@link org.eclipse.jetty.websocket.api.Session.Listener}. * * @author Rossen Stoyanchev * @since 4.0 @@ -132,12 +132,12 @@ public void onWebSocketError(Throwable cause) { private void tryCloseWithError(Throwable t) { - if (nativeSession.isOpen()) { + if (this.nativeSession.isOpen()) { ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); } else { // Session might be O-SHUT waiting for response close frame, so abort to close the connection. - nativeSession.disconnect(); + this.nativeSession.disconnect(); } } From 79393145aeedaa48b2379a6278281c1002f75b99 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 12 Feb 2024 21:50:19 +1100 Subject: [PATCH 118/146] Ensure DataBuffer is processed correctly for BINARY messages. Signed-off-by: Lachlan Roberts --- .../reactive/JettyCoreServerHttpResponse.java | 1 + .../socket/adapter/JettyWebSocketSession.java | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index ced7f42c7ddd..bb6736b1fe90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -132,6 +132,7 @@ public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); mono = Mono.fromFuture(callback); try { + // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 2f1ff6728e2f..68c0ef982c6f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -22,6 +22,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; import org.reactivestreams.Publisher; @@ -175,7 +176,6 @@ public Mono send(Publisher messages) { protected Mono sendMessage(WebSocketMessage message) { Callback.Completable completable = new Callback.Completable(); - DataBuffer dataBuffer = message.getPayload(); Session session = getDelegate(); if (WebSocketMessage.Type.TEXT.equals(message.getType())) { @@ -185,12 +185,32 @@ protected Mono sendMessage(WebSocketMessage message) { else { switch (message.getType()) { case BINARY -> { - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - session.sendBinary(byteBuffer, completable); + @SuppressWarnings("resource") + DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers(); + new IteratingCallback() { + @Override + protected Action process() { + if (!iterator.hasNext()) + return Action.SUCCEEDED; + + ByteBuffer buffer = iterator.next(); + boolean last = iterator.hasNext(); + session.sendPartialBinary(buffer, last, Callback.from(this::succeeded, this::failed)); + return Action.SCHEDULED; } - } + + @Override + protected void onCompleteSuccess() { + iterator.close(); + completable.complete(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) { + iterator.close(); + completable.completeExceptionally(cause); + } + }.iterate(); } case PING -> { // Maximum size of Control frame payload is 125, per RFC 6455. From 30a7c41ea6270a3ce1686e009ed48d45db44f757 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 13 Feb 2024 13:06:54 +1100 Subject: [PATCH 119/146] Wait for close of Session before calling handlerCompletionSink Signed-off-by: Lachlan Roberts --- .../socket/adapter/JettyWebSocketSession.java | 44 ++++++++++++++----- .../socket/client/JettyWebSocketClient.java | 8 ++++ ...ractReactiveWebSocketIntegrationTests.java | 4 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 68c0ef982c6f..8565bea6f305 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -128,20 +129,41 @@ void handleClose(CloseStatus closeStatus) { this.sink.complete(); } - void onHandlerError(Throwable ex) { - if (this.handlerCompletionSink != null) { - // Ignore result: can't overflow, ok if not first or no one listens - this.handlerCompletionSink.tryEmitError(ex); - } - close(CloseStatus.SERVER_ERROR); + void onHandlerError(Throwable error) { + getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), new Callback() { + @Override + public void succeed() { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + } + + @Override + public void fail(Throwable ex) { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + error.addSuppressed(ex); + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + } + }); } void onHandleComplete() { - if (this.handlerCompletionSink != null) { - // Ignore result: can't overflow, ok if not first or no one listens - this.handlerCompletionSink.tryEmitEmpty(); - } - close(); + getDelegate().close(StatusCode.NORMAL, null, new Callback() { + @Override + public void succeed() { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + } + + @Override + public void fail(Throwable ex) { + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + } + }); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index c47e5fc8d4d3..73b24a7a9633 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -50,6 +50,14 @@ public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient c this.client = client; } + public void start() throws Exception { + this.client.start(); + } + + public void stop() throws Exception { + this.client.stop(); + } + @Override public Mono execute(URI url, WebSocketHandler handler) { return execute(url, null, handler); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 9392f0cca98e..5b9e6b9b4654 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -89,11 +89,13 @@ abstract class AbstractReactiveWebSocketIntegrationTests { @interface ParameterizedWebSocketTest { } + private static final JettyWebSocketClient jettyClient = new JettyWebSocketClient(); + static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), - new JettyWebSocketClient(), + jettyClient, new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From 7ccb2e1bb98879157ca36a7c5eedd6c10118de9f Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 13 Feb 2024 13:10:55 +1100 Subject: [PATCH 120/146] fix checkstyle error Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 8565bea6f305..10f16c6722b3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -212,8 +212,9 @@ protected Mono sendMessage(WebSocketMessage message) { new IteratingCallback() { @Override protected Action process() { - if (!iterator.hasNext()) + if (!iterator.hasNext()) { return Action.SUCCEEDED; + } ByteBuffer buffer = iterator.next(); boolean last = iterator.hasNext(); From 38ef5bb6a509b8138cfb02e0d0309b4b061ea9d8 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 28 Feb 2024 10:42:58 +1100 Subject: [PATCH 121/146] Changes from review. Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 4 ++-- .../web/reactive/socket/client/JettyWebSocketClient.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 10f16c6722b3..90325cc44d3c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -225,13 +225,13 @@ protected Action process() { @Override protected void onCompleteSuccess() { iterator.close(); - completable.complete(null); + completable.succeed(); } @Override protected void onCompleteFailure(Throwable cause) { iterator.close(); - completable.completeExceptionally(cause); + completable.fail(cause); } }.iterate(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 73b24a7a9633..3a2b33668bfd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -78,7 +78,7 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl public void onHandshakeResponse(Request request, Response response) { String protocol = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); HttpHeaders responseHeaders = new HttpHeaders(); - response.getHeaders().forEach(header -> responseHeaders.addAll(header.getName(), header.getValueList())); + response.getHeaders().forEach(header -> responseHeaders.add(header.getName(), header.getValue())); handshakeInfo.set(new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol)); } }; @@ -89,6 +89,8 @@ public void onHandshakeResponse(Request request, Response response) { try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) .whenComplete((session, throwable) -> { + // Only fail the completion if we have an error + // as the JettyWebSocketSession will never be opened. if (throwable != null) { completion.tryEmitError(throwable); } From ea1dd82621a15ae38c0d0c1367e73ac87680f3f1 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 9 Apr 2024 14:51:17 +1000 Subject: [PATCH 122/146] Implement LifeCycle for JettyWebSocketClient Signed-off-by: Lachlan Roberts --- .../socket/client/JettyWebSocketClient.java | 19 +++++++++++-------- ...ractReactiveWebSocketIntegrationTests.java | 4 +--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 3a2b33668bfd..c5889b5abcdb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import org.springframework.context.Lifecycle; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; @@ -37,25 +38,27 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; -public class JettyWebSocketClient implements WebSocketClient { +public class JettyWebSocketClient implements WebSocketClient, Lifecycle { private final org.eclipse.jetty.websocket.client.WebSocketClient client; public JettyWebSocketClient() { this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); - LifeCycle.start(this.client); } - public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { - this.client = client; + @Override + public void start() { + LifeCycle.start(this.client); } - public void start() throws Exception { - this.client.start(); + @Override + public void stop() { + LifeCycle.stop(this.client); } - public void stop() throws Exception { - this.client.stop(); + @Override + public boolean isRunning() { + return false; } @Override diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java index 5b9e6b9b4654..9392f0cca98e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractReactiveWebSocketIntegrationTests.java @@ -89,13 +89,11 @@ abstract class AbstractReactiveWebSocketIntegrationTests { @interface ParameterizedWebSocketTest { } - private static final JettyWebSocketClient jettyClient = new JettyWebSocketClient(); - static Stream arguments() throws IOException { WebSocketClient[] clients = new WebSocketClient[] { new TomcatWebSocketClient(), - jettyClient, + new JettyWebSocketClient(), new ReactorNettyWebSocketClient(), new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY)) }; From e32983b2774e7990f026eaa58185fcc4a615b43b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Tue, 9 Apr 2024 15:09:53 +1000 Subject: [PATCH 123/146] additional changes from review Signed-off-by: Lachlan Roberts --- .../socket/adapter/JettyWebSocketSession.java | 51 +++++-------------- .../socket/client/JettyWebSocketClient.java | 15 +++--- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 90325cc44d3c..3db75639f49b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -55,7 +55,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private final Sinks.One closeStatusSink = Sinks.one(); private final Lock lock = new ReentrantLock(); private long requested = 0; - private boolean awaitingDemand = false; + private boolean awaitingMessage = false; @SuppressWarnings("NotNullFieldNotInitialized") private FluxSink sink; @@ -79,9 +79,9 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.lock.lock(); try { this.requested += n; - if (!this.awaitingDemand && this.requested > 0) { + if (!this.awaitingMessage && this.requested > 0) { this.requested--; - this.awaitingDemand = true; + this.awaitingMessage = true; demand = true; } } @@ -102,13 +102,13 @@ void handleMessage(WebSocketMessage message) { boolean demand = false; this.lock.lock(); try { - if (!this.awaitingDemand) { + if (!this.awaitingMessage) { throw new IllegalStateException(); } - this.awaitingDemand = false; + this.awaitingMessage = false; if (this.requested > 0) { this.requested--; - this.awaitingDemand = true; + this.awaitingMessage = true; demand = true; } } @@ -130,40 +130,17 @@ void handleClose(CloseStatus closeStatus) { } void onHandlerError(Throwable error) { - getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), new Callback() { - @Override - public void succeed() { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); - } - } - - @Override - public void fail(Throwable ex) { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - error.addSuppressed(ex); - JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); - } - } - }); + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitError(error); + } + getDelegate().close(StatusCode.SERVER_ERROR, error.getMessage(), Callback.NOOP); } void onHandleComplete() { - getDelegate().close(StatusCode.NORMAL, null, new Callback() { - @Override - public void succeed() { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); - } - } - - @Override - public void fail(Throwable ex) { - if (JettyWebSocketSession.this.handlerCompletionSink != null) { - JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); - } - } - }); + if (JettyWebSocketSession.this.handlerCompletionSink != null) { + JettyWebSocketSession.this.handlerCompletionSink.tryEmitEmpty(); + } + getDelegate().close(StatusCode.NORMAL, null, Callback.NOOP); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index c5889b5abcdb..ae2b185fe4ea 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -43,7 +43,11 @@ public class JettyWebSocketClient implements WebSocketClient, Lifecycle { private final org.eclipse.jetty.websocket.client.WebSocketClient client; public JettyWebSocketClient() { - this.client = new org.eclipse.jetty.websocket.client.WebSocketClient(); + this(new org.eclipse.jetty.websocket.client.WebSocketClient()); + } + + public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { + this.client = client; } @Override @@ -58,7 +62,7 @@ public void stop() { @Override public boolean isRunning() { - return false; + return this.client.isRunning(); } @Override @@ -91,12 +95,11 @@ public void onHandshakeResponse(Request request, Response response) { new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) - .whenComplete((session, throwable) -> { + .exceptionally((throwable) -> { // Only fail the completion if we have an error // as the JettyWebSocketSession will never be opened. - if (throwable != null) { - completion.tryEmitError(throwable); - } + completion.tryEmitError(throwable); + return null; }); return completion.asMono(); } From 077b60f05879130dcd4422b9597a4a8bcf88c241 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 29 May 2024 14:26:30 +1000 Subject: [PATCH 124/146] Use abstract request/response classes --- .../reactive/JettyCoreServerHttpRequest.java | 122 ++++-------- .../reactive/JettyCoreServerHttpResponse.java | 179 +++++------------- .../reactive/ServerHttpRequestDecorator.java | 1 + .../reactive/ServerHttpResponseDecorator.java | 1 + .../server/DefaultServerRequestBuilder.java | 5 - .../adapter/JettyWebSocketHandlerAdapter.java | 3 +- 6 files changed, 87 insertions(+), 224 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 59bf4d0d28c0..34f30021e46f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -17,7 +17,6 @@ package org.springframework.http.server.reactive; import java.net.InetSocketAddress; -import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; @@ -37,7 +36,6 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.RequestPath; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -45,15 +43,13 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; -import static org.springframework.http.server.reactive.AbstractServerHttpRequest.QUERY_PATTERN; - /** * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. * * @author Greg Wilkins * @since 6.2 */ -class JettyCoreServerHttpRequest implements ServerHttpRequest { +class JettyCoreServerHttpRequest extends AbstractServerHttpRequest { private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private static final MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); @@ -62,120 +58,80 @@ class JettyCoreServerHttpRequest implements ServerHttpRequest { private final Request request; - private final HttpHeaders headers; - - private final RequestPath path; - - @Nullable - private URI uri; - - @Nullable - MultiValueMap queryParameters; - - @Nullable - private MultiValueMap cookies; - public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { + super(HttpMethod.valueOf(request.getMethod()), + request.getHttpURI().toURI(), + request.getContext().getContextPath(), + new HttpHeaders(new JettyHeadersAdapter(request.getHeaders()))); this.dataBufferFactory = dataBufferFactory; this.request = request; - this.headers = new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())); - this.path = RequestPath.parse(request.getHttpURI().getPath(), request.getContext().getContextPath()); - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public HttpMethod getMethod() { - return HttpMethod.valueOf(this.request.getMethod()); - } - - @Override - public URI getURI() { - if (this.uri == null) { - this.uri = this.request.getHttpURI().toURI(); - } - return this.uri; } @Override public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) - .map(this::wrap); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::chunkToDataBuffer); } - private DataBuffer wrap(Content.Chunk chunk) { + private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } @Override - public String getId() { + protected String initId() { return this.request.getId(); } @Override - public RequestPath getPath() { - return this.path; - } + protected MultiValueMap initQueryParams() { + String query = this.request.getHttpURI().getQuery(); + if (StringUtil.isBlank(query)) { + return EMPTY_QUERY; + } - @Override - public MultiValueMap getQueryParams() { - if (this.queryParameters == null) { - String query = this.request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) { - this.queryParameters = EMPTY_QUERY; - } - else { - this.queryParameters = new LinkedMultiValueMap<>(); - Matcher matcher = QUERY_PATTERN.matcher(query); - while (matcher.find()) { - String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - String eq = matcher.group(2); - String value = matcher.group(3); - value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - this.queryParameters.add(name, value); - } - } + MultiValueMap map = new LinkedMultiValueMap<>(); + Matcher matcher = QUERY_PATTERN.matcher(query); + while (matcher.find()) { + String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + String eq = matcher.group(2); + String value = matcher.group(3); + value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); + map.add(name, value); } - return this.queryParameters; + return map; } @Override - public MultiValueMap getCookies() { - if (this.cookies == null) { - List httpCookies = Request.getCookies(this.request); - if (httpCookies.isEmpty()) { - this.cookies = EMPTY_COOKIES; - } - else { - this.cookies = new LinkedMultiValueMap<>(); - for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { - this.cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); - } - this.cookies = CollectionUtils.unmodifiableMultiValueMap(this.cookies); - } + protected MultiValueMap initCookies() { + List httpCookies = Request.getCookies(this.request); + if (httpCookies.isEmpty()) { + return EMPTY_COOKIES; + } + + MultiValueMap map =new LinkedMultiValueMap<>(); + for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { + map.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); } - return this.cookies; + + return map; } @Override + @Nullable public InetSocketAddress getLocalAddress() { - return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet - ? inet : null; + return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override + @Nullable public InetSocketAddress getRemoteAddress() { - return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet - ? inet : null; + return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet ? inet : null; } @Override - public SslInfo getSslInfo() { + @Nullable + public SslInfo initSslInfo() { if (this.request.getConnectionMetaData().isSecure() && this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { return new SslInfo() { @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bb6736b1fe90..be668d03ee50 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,9 +26,6 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -46,7 +43,6 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -55,8 +51,6 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -65,72 +59,73 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { - private final AtomicBoolean committed = new AtomicBoolean(false); - - private final List>> commitActions = new CopyOnWriteArrayList<>(); +class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { private final Response response; - private final HttpHeaders headers; - - @Nullable - private LinkedMultiValueMap cookies; - public JettyCoreServerHttpResponse(Response response) { - this.response = response; - this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + this(null, response); } - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public DataBufferFactory bufferFactory() { - return DefaultDataBufferFactory.sharedInstance; - } - - @Override - public void beforeCommit(Supplier> action) { - this.commitActions.add(action); - } + public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { + super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, + new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); + this.response = response; - @Override - public boolean isCommitted() { - return this.committed.get(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.addCookie(responseCookie); + i.remove(); + } + } } @Override - public Mono writeWith(Publisher body) { + protected Mono writeWithInternal(Publisher body) { return Flux.from(body) .flatMap(this::sendDataBuffer, 1) .then(); } @Override - public Mono writeAndFlushWith(Publisher> body) { + protected Mono writeAndFlushWithInternal(Publisher> body) { return Flux.from(body) .flatMap(this::writeWith, 1) .then(); } @Override - public Mono setComplete() { - Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; + protected void applyStatusCode() { + HttpStatusCode status = getStatusCode(); + this.response.setStatus(status == null ? 0 : status.value()); } @Override - public Mono writeWith(Path file, long position, long count) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> writeWith(file, position, count))); - } + protected void applyHeaders() { + + } + + @Override + protected void applyCookies() { + this.getCookies().values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); + } + @Override + public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); - mono = Mono.fromFuture(callback); + Mono mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -140,39 +135,10 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return mono; - } - - @Nullable - private Mono ensureCommitted() { - if (this.committed.compareAndSet(false, true)) { - if (!this.commitActions.isEmpty()) { - return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::writeCookies)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); - } - - writeCookies(); - } - - return null; - } - - private void writeCookies() { - if (this.cookies != null) { - this.cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); - } + return doCommit(() -> mono); } private Mono sendDataBuffer(DataBuffer dataBuffer) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); - } - @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -201,41 +167,7 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return Mono.fromFuture(callback); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) { - return false; - } - this.response.setStatus(status.value()); - return true; - } - - @Override - public HttpStatusCode getStatusCode() { - int status = this.response.getStatus(); - return HttpStatusCode.valueOf(status == 0 ? 200 : status); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) { - return false; - } - this.response.setStatus(value); - return true; - } - - @Override - public MultiValueMap getCookies() { - return initializeCookies(); - } - - @Override - public void addCookie(ResponseCookie cookie) { - initializeCookies().add(cookie.getName(), cookie); + return doCommit(() -> Mono.fromFuture(callback)); } @SuppressWarnings("unchecked") @@ -244,33 +176,10 @@ public T getNativeResponse() { return (T) this.response; } - private LinkedMultiValueMap initializeCookies() { - if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(); - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.cookies.add(responseCookie.getName(), responseCookie); - i.remove(); - } - } - } - return this.cookies; - } - - private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public HttpResponseCookie(ResponseCookie responseCookie) { + public ResponseHttpCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 559ff010efa6..46100e7edbbc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -109,6 +109,7 @@ public SslInfo getSslInfo() { } @Override + @Nullable public T getNativeRequest() { return this.delegate.getNativeRequest(); } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index d2ba47c747ed..3398a3416c0b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -124,6 +124,7 @@ public Mono setComplete() { } @Override + @Nullable public T getNativeResponse() { return getDelegate().getNativeResponse(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java index 07e7a1b61ec3..a97fbd902563 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilder.java @@ -297,11 +297,6 @@ public MultiValueMap getQueryParams() { public Flux getBody() { return this.body; } - - @Override - public T getNativeRequest() { - return null; - } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 6f42e85d7ec2..a8a747a6ddfe 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,6 +32,7 @@ import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -51,7 +52,7 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener { private final Function sessionFactory; - @SuppressWarnings("NotNullFieldNotInitialized") + @Nullable private JettyWebSocketSession delegateSession; public JettyWebSocketHandlerAdapter(WebSocketHandler handler, From fa5f800bc93652943b39f570729cef9a85505629 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 29 May 2024 15:58:55 +1000 Subject: [PATCH 125/146] Suppress NullAway warnings for Jetty WebSocket classes Signed-off-by: Lachlan Roberts --- .../socket/adapter/JettyWebSocketHandlerAdapter.java | 1 + .../reactive/socket/adapter/JettyWebSocketSession.java | 3 ++- .../adapter/jetty/JettyWebSocketHandlerAdapter.java | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index a8a747a6ddfe..584eddfe1cba 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -47,6 +47,7 @@ * @author Rossen Stoyanchev * @since 5.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { private final WebSocketHandler delegateHandler; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 3db75639f49b..9521b83ede7a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -49,6 +49,7 @@ * @author Rossen Stoyanchev * @since 5.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; @@ -57,7 +58,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private long requested = 0; private boolean awaitingMessage = false; - @SuppressWarnings("NotNullFieldNotInitialized") + @Nullable private FluxSink sink; @Nullable diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index 959ab677f1be..3322fc49f566 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.socket.BinaryMessage; import org.springframework.web.socket.CloseStatus; @@ -38,14 +39,15 @@ * @author Rossen Stoyanchev * @since 4.0 */ +@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class); private final WebSocketHandler webSocketHandler; private final JettyWebSocketSession wsSession; - @SuppressWarnings("NotNullFieldNotInitialized") - private Session nativeSession; + @Nullable + private Session nativeSession; public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { Assert.notNull(webSocketHandler, "WebSocketHandler must not be null"); @@ -54,7 +56,6 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebS this.wsSession = wsSession; } - @Override public void onWebSocketOpen(Session session) { try { @@ -140,5 +141,4 @@ private void tryCloseWithError(Throwable t) { this.nativeSession.disconnect(); } } - } From 442f11c19ab13504c0cc976916dda9ceee7f6d71 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Wed, 29 May 2024 16:09:59 +1000 Subject: [PATCH 126/146] fix other checkstyle warnings Signed-off-by: Lachlan Roberts --- .../web/reactive/socket/client/JettyWebSocketClient.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index ae2b185fe4ea..bc770de68147 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.Request; @@ -79,7 +80,7 @@ public Mono execute(URI url, @Nullable HttpHeaders headers, WebSocketHandl headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); } - AtomicReference handshakeInfo = new AtomicReference<>(); + final AtomicReference handshakeInfo = new AtomicReference<>(); JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { @Override public void onHandshakeResponse(Request request, Response response) { @@ -92,10 +93,10 @@ public void onHandshakeResponse(Request request, Response response) { Sinks.Empty completion = Sinks.empty(); JettyWebSocketHandlerAdapter handlerAdapter = new JettyWebSocketHandlerAdapter(handler, session -> - new JettyWebSocketSession(session, handshakeInfo.get(), DefaultDataBufferFactory.sharedInstance, completion)); + new JettyWebSocketSession(session, Objects.requireNonNull(handshakeInfo.get()), DefaultDataBufferFactory.sharedInstance, completion)); try { this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) - .exceptionally((throwable) -> { + .exceptionally(throwable -> { // Only fail the completion if we have an error // as the JettyWebSocketSession will never be opened. completion.tryEmitError(throwable); From bd25bd74a7a67a169bec619c547e903fd35017f7 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 29 May 2024 22:56:05 +1000 Subject: [PATCH 127/146] backed out abstract response change upgrade jetty --- .../reactive/JettyCoreServerHttpResponse.java | 179 +++++++++++++----- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index be668d03ee50..bb6736b1fe90 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,6 +26,9 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -43,6 +46,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -51,6 +55,8 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -59,73 +65,72 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { +class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { + private final AtomicBoolean committed = new AtomicBoolean(false); + + private final List>> commitActions = new CopyOnWriteArrayList<>(); private final Response response; - public JettyCoreServerHttpResponse(Response response) { - this(null, response); - } + private final HttpHeaders headers; + + @Nullable + private LinkedMultiValueMap cookies; - public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { - super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, - new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); + public JettyCoreServerHttpResponse(Response response) { this.response = response; + this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + } - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.addCookie(responseCookie); - i.remove(); - } - } + @Override + public HttpHeaders getHeaders() { + return this.headers; } @Override - protected Mono writeWithInternal(Publisher body) { - return Flux.from(body) - .flatMap(this::sendDataBuffer, 1) - .then(); + public DataBufferFactory bufferFactory() { + return DefaultDataBufferFactory.sharedInstance; } @Override - protected Mono writeAndFlushWithInternal(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); + public void beforeCommit(Supplier> action) { + this.commitActions.add(action); } @Override - protected void applyStatusCode() { - HttpStatusCode status = getStatusCode(); - this.response.setStatus(status == null ? 0 : status.value()); + public boolean isCommitted() { + return this.committed.get(); } @Override - protected void applyHeaders() { + public Mono writeWith(Publisher body) { + return Flux.from(body) + .flatMap(this::sendDataBuffer, 1) + .then(); + } + @Override + public Mono writeAndFlushWith(Publisher> body) { + return Flux.from(body) + .flatMap(this::writeWith, 1) + .then(); } @Override - protected void applyCookies() { - this.getCookies().values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); + public Mono setComplete() { + Mono mono = ensureCommitted(); + return (mono == null) ? Mono.empty() : mono; } @Override public Mono writeWith(Path file, long position, long count) { + Mono mono = ensureCommitted(); + if (mono != null) { + return mono.then(Mono.defer(() -> writeWith(file, position, count))); + } + Callback.Completable callback = new Callback.Completable(); - Mono mono = Mono.fromFuture(callback); + mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -135,10 +140,39 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return doCommit(() -> mono); + return mono; + } + + @Nullable + private Mono ensureCommitted() { + if (this.committed.compareAndSet(false, true)) { + if (!this.commitActions.isEmpty()) { + return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) + .concatWith(Mono.fromRunnable(this::writeCookies)) + .then() + .doOnError(t -> getHeaders().clearContentHeaders()); + } + + writeCookies(); + } + + return null; + } + + private void writeCookies() { + if (this.cookies != null) { + this.cookies.values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); + } } private Mono sendDataBuffer(DataBuffer dataBuffer) { + Mono mono = ensureCommitted(); + if (mono != null) { + return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); + } + @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -167,7 +201,41 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return doCommit(() -> Mono.fromFuture(callback)); + return Mono.fromFuture(callback); + } + + @Override + public boolean setStatusCode(@Nullable HttpStatusCode status) { + if (isCommitted() || status == null) { + return false; + } + this.response.setStatus(status.value()); + return true; + } + + @Override + public HttpStatusCode getStatusCode() { + int status = this.response.getStatus(); + return HttpStatusCode.valueOf(status == 0 ? 200 : status); + } + + @Override + public boolean setRawStatusCode(@Nullable Integer value) { + if (isCommitted() || value == null) { + return false; + } + this.response.setStatus(value); + return true; + } + + @Override + public MultiValueMap getCookies() { + return initializeCookies(); + } + + @Override + public void addCookie(ResponseCookie cookie) { + initializeCookies().add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -176,10 +244,33 @@ public T getNativeResponse() { return (T) this.response; } - private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { + private LinkedMultiValueMap initializeCookies() { + if (this.cookies == null) { + this.cookies = new LinkedMultiValueMap<>(); + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.cookies.add(responseCookie.getName(), responseCookie); + i.remove(); + } + } + } + return this.cookies; + } + + private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public ResponseHttpCookie(ResponseCookie responseCookie) { + public HttpResponseCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } From 3785321afad1d2b6750651c8e227e2405baf1e11 Mon Sep 17 00:00:00 2001 From: gregw Date: Thu, 30 May 2024 06:57:42 +1000 Subject: [PATCH 128/146] Steps towards abstract response class --- .../reactive/JettyCoreServerHttpResponse.java | 74 ++++++++----------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index bb6736b1fe90..e7eb50da8808 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -74,12 +74,30 @@ class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOut private final HttpHeaders headers; - @Nullable - private LinkedMultiValueMap cookies; + private final LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); + + private @Nullable HttpStatusCode status; public JettyCoreServerHttpResponse(Response response) { this.response = response; this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); + + // remove all existing cookies from the response and add them to the cookie map, to be added back later + for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { + HttpField f = i.next(); + if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { + HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); + ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) + .httpOnly(httpCookie.isHttpOnly()) + .domain(httpCookie.getDomain()) + .maxAge(httpCookie.getMaxAge()) + .sameSite(httpCookie.getSameSite().name()) + .secure(httpCookie.isSecure()) + .build(); + this.addCookie(responseCookie); + i.remove(); + } + } } @Override @@ -146,6 +164,8 @@ public Mono writeWith(Path file, long position, long count) { @Nullable private Mono ensureCommitted() { if (this.committed.compareAndSet(false, true)) { + if (this.status != null) + this.response.setStatus(this.status.value()); if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -163,7 +183,7 @@ private void writeCookies() { if (this.cookies != null) { this.cookies.values().stream() .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new HttpResponseCookie(cookie))); + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); } } @@ -206,36 +226,27 @@ protected void onCompleteFailure(Throwable cause) { @Override public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted() || status == null) { + if (isCommitted()) { return false; } - this.response.setStatus(status.value()); + this.status = status; return true; } @Override + @Nullable public HttpStatusCode getStatusCode() { - int status = this.response.getStatus(); - return HttpStatusCode.valueOf(status == 0 ? 200 : status); - } - - @Override - public boolean setRawStatusCode(@Nullable Integer value) { - if (isCommitted() || value == null) { - return false; - } - this.response.setStatus(value); - return true; + return status; } @Override public MultiValueMap getCookies() { - return initializeCookies(); + return this.cookies; } @Override public void addCookie(ResponseCookie cookie) { - initializeCookies().add(cookie.getName(), cookie); + this.cookies.add(cookie.getName(), cookie); } @SuppressWarnings("unchecked") @@ -244,33 +255,10 @@ public T getNativeResponse() { return (T) this.response; } - private LinkedMultiValueMap initializeCookies() { - if (this.cookies == null) { - this.cookies = new LinkedMultiValueMap<>(); - // remove all existing cookies from the response and add them to the cookie map, to be added back later - for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { - HttpField f = i.next(); - if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { - HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); - ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) - .httpOnly(httpCookie.isHttpOnly()) - .domain(httpCookie.getDomain()) - .maxAge(httpCookie.getMaxAge()) - .sameSite(httpCookie.getSameSite().name()) - .secure(httpCookie.isSecure()) - .build(); - this.cookies.add(responseCookie.getName(), responseCookie); - i.remove(); - } - } - } - return this.cookies; - } - - private static class HttpResponseCookie implements org.eclipse.jetty.http.HttpCookie { + private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { private final ResponseCookie responseCookie; - public HttpResponseCookie(ResponseCookie responseCookie) { + public ResponseHttpCookie(ResponseCookie responseCookie) { this.responseCookie = responseCookie; } From d8eb3fae02fcac36624b28d3a166512f79c4ea92 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 09:00:11 +1000 Subject: [PATCH 129/146] checkstyle --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index e7eb50da8808..a29d5ff437c9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -164,8 +164,9 @@ public Mono writeWith(Path file, long position, long count) { @Nullable private Mono ensureCommitted() { if (this.committed.compareAndSet(false, true)) { - if (this.status != null) + if (this.status != null) { this.response.setStatus(this.status.value()); + } if (!this.commitActions.isEmpty()) { return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) .concatWith(Mono.fromRunnable(this::writeCookies)) @@ -236,7 +237,7 @@ public boolean setStatusCode(@Nullable HttpStatusCode status) { @Override @Nullable public HttpStatusCode getStatusCode() { - return status; + return this.status; } @Override From c6b191402058ca42f2554730ada0954cc8aa1034 Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 09:40:57 +1000 Subject: [PATCH 130/146] updated response to use the abstract --- .../reactive/JettyCoreServerHttpResponse.java | 133 ++++-------------- 1 file changed, 26 insertions(+), 107 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index a29d5ff437c9..7e98a67e4cbc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -26,9 +26,6 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -42,11 +39,11 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; @@ -55,8 +52,6 @@ import org.springframework.http.ZeroCopyHttpOutputMessage; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.lang.Nullable; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; /** * Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. @@ -65,24 +60,20 @@ * @author Lachlan Roberts * @since 6.2 */ -class JettyCoreServerHttpResponse implements ServerHttpResponse, ZeroCopyHttpOutputMessage { - private final AtomicBoolean committed = new AtomicBoolean(false); - - private final List>> commitActions = new CopyOnWriteArrayList<>(); +class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { private final Response response; - private final HttpHeaders headers; - - private final LinkedMultiValueMap cookies = new LinkedMultiValueMap<>(); - - private @Nullable HttpStatusCode status; - public JettyCoreServerHttpResponse(Response response) { + this(null, response); + } + + public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { + super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, + new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); this.response = response; - this.headers = new HttpHeaders(new JettyHeadersAdapter(response.getHeaders())); - // remove all existing cookies from the response and add them to the cookie map, to be added back later + // remove all existing cookies from the response and add them to the cookie map, to be added back later for (ListIterator i = this.response.getHeaders().listIterator(); i.hasNext(); ) { HttpField f = i.next(); if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { @@ -101,54 +92,39 @@ public JettyCoreServerHttpResponse(Response response) { } @Override - public HttpHeaders getHeaders() { - return this.headers; - } - - @Override - public DataBufferFactory bufferFactory() { - return DefaultDataBufferFactory.sharedInstance; + protected Mono writeWithInternal(Publisher body) { + return Flux.from(body) + .flatMap(this::sendDataBuffer, 1) + .then(); } @Override - public void beforeCommit(Supplier> action) { - this.commitActions.add(action); + protected Mono writeAndFlushWithInternal(Publisher> body) { + return Flux.from(body).flatMap(this::writeWithInternal, 1).then(); } @Override - public boolean isCommitted() { - return this.committed.get(); + protected void applyStatusCode() { + HttpStatusCode status = getStatusCode(); + this.response.setStatus(status == null ? 0 : status.value()); } @Override - public Mono writeWith(Publisher body) { - return Flux.from(body) - .flatMap(this::sendDataBuffer, 1) - .then(); - } + protected void applyHeaders() { - @Override - public Mono writeAndFlushWith(Publisher> body) { - return Flux.from(body) - .flatMap(this::writeWith, 1) - .then(); } @Override - public Mono setComplete() { - Mono mono = ensureCommitted(); - return (mono == null) ? Mono.empty() : mono; + protected void applyCookies() { + this.getCookies().values().stream() + .flatMap(List::stream) + .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); } @Override public Mono writeWith(Path file, long position, long count) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> writeWith(file, position, count))); - } - Callback.Completable callback = new Callback.Completable(); - mono = Mono.fromFuture(callback); + Mono mono = Mono.fromFuture(callback); try { // The method can block, but it is not expected to do so for any significant time. @SuppressWarnings("BlockingMethodInNonBlockingContext") @@ -158,42 +134,10 @@ public Mono writeWith(Path file, long position, long count) { catch (Throwable th) { callback.failed(th); } - return mono; - } - - @Nullable - private Mono ensureCommitted() { - if (this.committed.compareAndSet(false, true)) { - if (this.status != null) { - this.response.setStatus(this.status.value()); - } - if (!this.commitActions.isEmpty()) { - return Flux.concat(Flux.fromIterable(this.commitActions).map(Supplier::get)) - .concatWith(Mono.fromRunnable(this::writeCookies)) - .then() - .doOnError(t -> getHeaders().clearContentHeaders()); - } - - writeCookies(); - } - - return null; - } - - private void writeCookies() { - if (this.cookies != null) { - this.cookies.values().stream() - .flatMap(List::stream) - .forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); - } + return doCommit(() -> mono); } private Mono sendDataBuffer(DataBuffer dataBuffer) { - Mono mono = ensureCommitted(); - if (mono != null) { - return mono.then(Mono.defer(() -> sendDataBuffer(dataBuffer))); - } - @SuppressWarnings("resource") DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); Callback.Completable callback = new Callback.Completable(); @@ -222,32 +166,7 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return Mono.fromFuture(callback); - } - - @Override - public boolean setStatusCode(@Nullable HttpStatusCode status) { - if (isCommitted()) { - return false; - } - this.status = status; - return true; - } - - @Override - @Nullable - public HttpStatusCode getStatusCode() { - return this.status; - } - - @Override - public MultiValueMap getCookies() { - return this.cookies; - } - - @Override - public void addCookie(ResponseCookie cookie) { - this.cookies.add(cookie.getName(), cookie); + return doCommit(() -> Mono.fromFuture(callback)); } @SuppressWarnings("unchecked") From a2420fc23c05cc3da2db8246b31922bbc0a5701a Mon Sep 17 00:00:00 2001 From: gregw Date: Fri, 31 May 2024 10:06:38 +1000 Subject: [PATCH 131/146] updated response to use the abstract --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 7e98a67e4cbc..4a3332d3e4bf 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -39,7 +39,6 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; From 5fbf83c3e7e65fe8fac84659e40626959ec44990 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Thu, 30 May 2024 13:46:15 +1000 Subject: [PATCH 132/146] add explicit dependency on jetty-websocket-api Signed-off-by: Lachlan Roberts --- spring-websocket/spring-websocket.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-websocket/spring-websocket.gradle b/spring-websocket/spring-websocket.gradle index 72df03b20dd0..2250f5fdf38b 100644 --- a/spring-websocket/spring-websocket.gradle +++ b/spring-websocket/spring-websocket.gradle @@ -19,6 +19,7 @@ dependencies { optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" } + optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") { exclude group: "jakarta.servlet", module: "jakarta.servlet-api" From 9b7a2f47995cd14a6c42c9974de96ca7d1c349bc Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 29 May 2024 11:12:04 +0200 Subject: [PATCH 133/146] Polishing --- spring-jcl/spring-jcl.gradle | 2 - .../reactive/AbstractServerHttpRequest.java | 9 +- .../reactive/AbstractServerHttpResponse.java | 8 ++ .../DefaultServerHttpRequestBuilder.java | 32 ++--- .../reactive/JettyCoreHttpHandlerAdapter.java | 44 ++----- .../reactive/JettyCoreServerHttpRequest.java | 116 ++++++------------ .../reactive/JettyCoreServerHttpResponse.java | 12 +- .../server/reactive/ServerHttpRequest.java | 10 -- .../reactive/ServerHttpRequestDecorator.java | 17 ++- .../server/reactive/ServerHttpResponse.java | 9 -- .../reactive/ServerHttpResponseDecorator.java | 16 ++- .../result/view/ZeroDemandResponse.java | 4 - 12 files changed, 90 insertions(+), 189 deletions(-) diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle index 560d85324cb0..d609737b2551 100644 --- a/spring-jcl/spring-jcl.gradle +++ b/spring-jcl/spring-jcl.gradle @@ -3,6 +3,4 @@ description = "Spring Commons Logging Bridge" dependencies { optional("org.apache.logging.log4j:log4j-api") optional("org.slf4j:slf4j-api") - optional("biz.aQute.bnd:biz.aQute.bnd.annotation:6.3.1") - optional("org.osgi:osgi.annotation:8.1.0") } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 9e4f1547e171..829a2202a814 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -42,7 +42,7 @@ */ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { - static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); + private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); private final URI uri; @@ -203,6 +203,13 @@ public SslInfo getSslInfo() { @Nullable protected abstract SslInfo initSslInfo(); + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + public abstract T getNativeRequest(); + /** * For internal use in logging at the HTTP adapter layer. * @since 5.1 diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 82731c218d90..7a06126ce85d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -155,6 +155,14 @@ public void addCookie(ResponseCookie cookie) { } } + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + public abstract T getNativeResponse(); + + @Override public void beforeCommit(Supplier> action) { this.commitActions.add(action); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index ef6a2fa1bf4d..a94e378e3c3c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -20,7 +20,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; -import java.util.Objects; import java.util.function.Consumer; import reactor.core.publisher.Flux; @@ -68,29 +67,14 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { - this(original.getURI(), - new HttpHeaders(original.getHeaders()), - original.getMethod(), - original.getPath().contextPath().value(), - original.getRemoteAddress(), - original.getBody(), - Objects.requireNonNull(original, "ServerHttpRequest is required")); - } - - public DefaultServerHttpRequestBuilder( - URI uri, - HttpHeaders httpHeaders, - HttpMethod method, - String contextPath, - @Nullable InetSocketAddress remoteAddress, - Flux body, - ServerHttpRequest original) { - this.uri = uri; - this.headers = httpHeaders; - this.httpMethod = method; - this.contextPath = contextPath; - this.remoteAddress = remoteAddress; - this.body = body; + Assert.notNull(original, "ServerHttpRequest is required"); + + this.uri = original.getURI(); + this.headers = new HttpHeaders(original.getHeaders()); + this.httpMethod = original.getMethod(); + this.contextPath = original.getPath().contextPath().value(); + this.remoteAddress = original.getRemoteAddress(); + this.body = original.getBody(); this.originalRequest = original; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index a687d03ea791..8a7b8e31c13e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,59 +20,41 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.util.Assert; /** * Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. * * @author Greg Wilkins * @author Lachlan Roberts + * @author Arjen Poutsma * @since 6.2 */ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; - private final DataBufferFactory dataBufferFactory; + private DataBufferFactory dataBufferFactory = DefaultDataBufferFactory.sharedInstance; + public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; + } - // Currently we do not make a DataBufferFactory over the servers ByteBufferPool, - // because we mainly use wrap and there should be few allocation done by the factory. - // But it could be possible to use the servers buffer pool for allocations and to - // create PooledDataBuffers - this.dataBufferFactory = new DefaultDataBufferFactory(); + public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { + Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); + this.dataBufferFactory = dataBufferFactory; } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - this.httpHandler.handle(new JettyCoreServerHttpRequest(this.dataBufferFactory, request), new JettyCoreServerHttpResponse(response)) - .subscribe(new Subscriber<>() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Void unused) { - // we can ignore the void as we only seek onError or onComplete - } - - @Override - public void onError(Throwable t) { - callback.failed(t); - } - - @Override - public void onComplete() { - callback.succeeded(); - } - }); + this.httpHandler.handle(new JettyCoreServerHttpRequest(request, this.dataBufferFactory), + new JettyCoreServerHttpResponse(response, this.dataBufferFactory)) + .subscribe(unused -> {}, callback::failed, callback::succeeded); return true; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 34f30021e46f..a0f4ffa2c058 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,13 @@ package org.springframework.http.server.reactive; import java.net.InetSocketAddress; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.cert.X509Certificate; +import java.net.SocketAddress; +import java.util.Collections; import java.util.List; -import java.util.regex.Matcher; -import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.util.StringUtil; import org.reactivestreams.FlowAdapters; import reactor.core.publisher.Flux; @@ -41,24 +37,22 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; /** * Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. * * @author Greg Wilkins + * @author Arjen Poutsma * @since 6.2 */ class JettyCoreServerHttpRequest extends AbstractServerHttpRequest { - private static final MultiValueMap EMPTY_QUERY = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); - - private static final MultiValueMap EMPTY_COOKIES = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>()); private final DataBufferFactory dataBufferFactory; private final Request request; - public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request request) { + + public JettyCoreServerHttpRequest(Request request, DataBufferFactory dataBufferFactory) { super(HttpMethod.valueOf(request.getMethod()), request.getHttpURI().toURI(), request.getContext().getContextPath(), @@ -67,85 +61,57 @@ public JettyCoreServerHttpRequest(DataBufferFactory dataBufferFactory, Request r this.request = request; } - @Override - public Flux getBody() { - // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and - // then wrapped as a Flux. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::chunkToDataBuffer); - } - - private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); - } - - @Override - protected String initId() { - return this.request.getId(); - } - - @Override - protected MultiValueMap initQueryParams() { - String query = this.request.getHttpURI().getQuery(); - if (StringUtil.isBlank(query)) { - return EMPTY_QUERY; - } - - MultiValueMap map = new LinkedMultiValueMap<>(); - Matcher matcher = QUERY_PATTERN.matcher(query); - while (matcher.find()) { - String name = URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - String eq = matcher.group(2); - String value = matcher.group(3); - value = (value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : (StringUtils.hasLength(eq) ? "" : null)); - map.add(name, value); - } - return map; - } - @Override protected MultiValueMap initCookies() { List httpCookies = Request.getCookies(this.request); if (httpCookies.isEmpty()) { - return EMPTY_COOKIES; + return CollectionUtils.toMultiValueMap(Collections.emptyMap()); } - - MultiValueMap map =new LinkedMultiValueMap<>(); + MultiValueMap cookies =new LinkedMultiValueMap<>(); for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { - map.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); + cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); } + return cookies; + } - return map; + @Override + @Nullable + public SslInfo initSslInfo() { + if (this.request.getConnectionMetaData().isSecure() && + this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sessionData) { + return new DefaultSslInfo(sessionData.sslSessionId(), sessionData.peerCertificates()); + } + return null; + } + + @Override + protected String initId() { + return this.request.getId(); } @Override @Nullable public InetSocketAddress getLocalAddress() { - return this.request.getConnectionMetaData().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : null; + SocketAddress localAddress = this.request.getConnectionMetaData().getLocalSocketAddress(); + return localAddress instanceof InetSocketAddress inet ? inet : null; } @Override @Nullable public InetSocketAddress getRemoteAddress() { - return this.request.getConnectionMetaData().getRemoteSocketAddress() instanceof InetSocketAddress inet ? inet : null; + SocketAddress remoteAddress = this.request.getConnectionMetaData().getRemoteSocketAddress(); + return remoteAddress instanceof InetSocketAddress inet ? inet : null; } @Override - @Nullable - public SslInfo initSslInfo() { - if (this.request.getConnectionMetaData().isSecure() && this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sslSessionData) { - return new SslInfo() { - @Override - public String getSessionId() { - return sslSessionData.sslSessionId(); - } - - @Override - public X509Certificate[] getPeerCertificates() { - return sslSessionData.peerCertificates(); - } - }; - } - return null; + public Flux getBody() { + // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and + // then wrapped as a Flux. + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::chunkToDataBuffer); + } + + private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { + return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); } @SuppressWarnings("unchecked") @@ -154,14 +120,4 @@ public T getNativeRequest() { return (T) this.request; } - @Override - public Builder mutate() { - return new DefaultServerHttpRequestBuilder(this.getURI(), - new HttpHeaders(new JettyHeadersAdapter(HttpFields.build(this.request.getHeaders()))), - this.getMethod(), - this.getPath().contextPath().value(), - this.getRemoteAddress(), - this.getBody(), - this); - } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 4a3332d3e4bf..4ae0b5b89e58 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -43,8 +43,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; @@ -63,13 +63,8 @@ class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements private final Response response; - public JettyCoreServerHttpResponse(Response response) { - this(null, response); - } - - public JettyCoreServerHttpResponse(@Nullable DefaultDataBufferFactory dataBufferFactory, Response response) { - super(dataBufferFactory == null ? DefaultDataBufferFactory.sharedInstance : dataBufferFactory, - new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); + public JettyCoreServerHttpResponse(Response response, DataBufferFactory dataBufferFactory) { + super(dataBufferFactory, new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); this.response = response; // remove all existing cookies from the response and add them to the cookie map, to be added back later @@ -110,7 +105,6 @@ protected void applyStatusCode() { @Override protected void applyHeaders() { - } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 41814c35aeb0..55f5fa2c654b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -97,16 +97,6 @@ default SslInfo getSslInfo() { return null; } - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - @Nullable - default T getNativeRequest() { - return null; - } - /** * Return a builder to mutate properties of this request by wrapping it * with {@link ServerHttpRequestDecorator} and returning either mutated diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 46100e7edbbc..fc6143bfdaf0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -108,12 +108,6 @@ public SslInfo getSslInfo() { return getDelegate().getSslInfo(); } - @Override - @Nullable - public T getNativeRequest() { - return this.delegate.getNativeRequest(); - } - @Override public Flux getBody() { return getDelegate().getBody(); @@ -129,13 +123,16 @@ public Flux getBody() { * @since 5.3.3 */ public static T getNativeRequest(ServerHttpRequest request) { - T nativeRequest = request.getNativeRequest(); - if (nativeRequest == null) { + if (request instanceof AbstractServerHttpRequest abstractServerHttpRequest) { + return abstractServerHttpRequest.getNativeRequest(); + } + else if (request instanceof ServerHttpRequestDecorator serverHttpRequestDecorator) { + return getNativeRequest(serverHttpRequestDecorator.getDelegate()); + } + else { throw new IllegalArgumentException( "Can't find native request in " + request.getClass().getName()); } - - return nativeRequest; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 5ab1708957a7..7f97d04484a5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -86,13 +86,4 @@ default Integer getRawStatusCode() { */ void addCookie(ResponseCookie cookie); - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - @Nullable - default T getNativeResponse() { - return null; - } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index 3398a3416c0b..d6f7f85b953c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -123,11 +123,6 @@ public Mono setComplete() { return getDelegate().setComplete(); } - @Override - @Nullable - public T getNativeResponse() { - return getDelegate().getNativeResponse(); - } /** * Return the native response of the underlying server API, if possible, @@ -138,13 +133,16 @@ public T getNativeResponse() { * @since 5.3.3 */ public static T getNativeResponse(ServerHttpResponse response) { - T nativeResponse = response.getNativeResponse(); - if (nativeResponse == null) { + if (response instanceof AbstractServerHttpResponse abstractServerHttpResponse) { + return abstractServerHttpResponse.getNativeResponse(); + } + else if (response instanceof ServerHttpResponseDecorator serverHttpResponseDecorator) { + return getNativeResponse(serverHttpResponseDecorator.getDelegate()); + } + else { throw new IllegalArgumentException( "Can't find native response in " + response.getClass().getName()); } - - return nativeResponse; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index c189651e433e..b543447e1e68 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -116,10 +116,6 @@ public HttpHeaders getHeaders() { throw new UnsupportedOperationException(); } - @Override - public T getNativeResponse() { - return null; - } private static class ZeroDemandSubscriber extends BaseSubscriber { From b8d208dae31e3fd6a7ad3fdbda42a415f8ba8115 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 31 May 2024 12:39:52 +0200 Subject: [PATCH 134/146] Replace flatMap(...,1) with concatMap --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 4ae0b5b89e58..fabbec9d19ee 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -88,13 +88,13 @@ public JettyCoreServerHttpResponse(Response response, DataBufferFactory dataBuff @Override protected Mono writeWithInternal(Publisher body) { return Flux.from(body) - .flatMap(this::sendDataBuffer, 1) + .concatMap(this::sendDataBuffer) .then(); } @Override protected Mono writeAndFlushWithInternal(Publisher> body) { - return Flux.from(body).flatMap(this::writeWithInternal, 1).then(); + return Flux.from(body).concatMap(this::writeWithInternal).then(); } @Override From db82c220074f68d45a516e0c57d8ba71c3b7de43 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 5 Jun 2024 13:06:20 +0200 Subject: [PATCH 135/146] Make copy of headers map --- .../http/server/reactive/DefaultServerHttpRequestBuilder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index a94e378e3c3c..59ae6eb3f5ce 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -30,6 +30,7 @@ import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -70,7 +71,8 @@ public DefaultServerHttpRequestBuilder(ServerHttpRequest original) { Assert.notNull(original, "ServerHttpRequest is required"); this.uri = original.getURI(); - this.headers = new HttpHeaders(original.getHeaders()); + // original headers can be immutable, so create a copy + this.headers = new HttpHeaders(new LinkedMultiValueMap<>(original.getHeaders())); this.httpMethod = original.getMethod(); this.contextPath = original.getPath().contextPath().value(); this.remoteAddress = original.getRemoteAddress(); From 16af65aacbeff0393537cfcdd8babd4a84204bb4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 5 Jun 2024 14:27:46 +0200 Subject: [PATCH 136/146] Introduce JettyDataBuffer --- spring-core/spring-core.gradle | 1 + .../core/io/buffer/DefaultDataBuffer.java | 2 +- .../core/io/buffer/JettyDataBuffer.java | 342 ++++++++++++++++++ .../io/buffer/JettyDataBufferFactory.java | 108 ++++++ .../core/io/buffer/JettyDataBufferTests.java | 59 +++ .../core/io/buffer/PooledDataBufferTests.java | 13 + .../reactive/JettyClientHttpConnector.java | 296 +-------------- .../reactive/JettyCoreHttpHandlerAdapter.java | 7 +- .../reactive/JettyCoreServerHttpRequest.java | 9 +- .../reactive/JettyCoreServerHttpResponse.java | 4 +- .../reactive/JettyRetainedDataBuffer.java | 285 --------------- 11 files changed, 538 insertions(+), 588 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java create mode 100644 spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java create mode 100644 spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java delete mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index fedd203d553c..91710c31c30b 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -81,6 +81,7 @@ dependencies { optional("io.smallrye.reactive:mutiny") optional("net.sf.jopt-simple:jopt-simple") optional("org.aspectj:aspectjweaver") + optional("org.eclipse.jetty:jetty-io") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 16b444dfb40f..d9d43da4eeaf 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -355,7 +355,7 @@ public DefaultDataBuffer slice(int index, int length) { } @Override - public DataBuffer split(int index) { + public DefaultDataBuffer split(int index) { checkIndex(index); ByteBuffer split = this.byteBuffer.duplicate().clear() diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java new file mode 100644 index 000000000000..b272174d2999 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java @@ -0,0 +1,342 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io.buffer; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntPredicate; + +import org.eclipse.jetty.io.Content; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Implementation of the {@code DataBuffer} interface that can wrap a Jetty + * {@link Content.Chunk}. Typically constructed with {@link JettyDataBufferFactory}. + * + * @author Greg Wilkins + * @author Lachlan Roberts + * @author Arjen Poutsma + * @since 6.2 + */ +public final class JettyDataBuffer implements PooledDataBuffer { + + private final DefaultDataBuffer delegate; + + @Nullable + private final Content.Chunk chunk; + + private final JettyDataBufferFactory bufferFactory; + + private final AtomicInteger refCount = new AtomicInteger(1); + + + JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate, Content.Chunk chunk) { + Assert.notNull(bufferFactory, "BufferFactory must not be null"); + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(chunk, "Chunk must not be null"); + + this.bufferFactory = bufferFactory; + this.delegate = delegate; + this.chunk = chunk; + } + + JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) { + Assert.notNull(bufferFactory, "BufferFactory must not be null"); + Assert.notNull(delegate, "Delegate must not be null"); + + this.bufferFactory = bufferFactory; + this.delegate = delegate; + this.chunk = null; + } + + @Override + public boolean isAllocated() { + return this.refCount.get() > 0; + } + + @Override + public PooledDataBuffer retain() { + int result = this.refCount.updateAndGet(c -> { + if (c != 0) { + return c + 1; + } + else { + return 0; + } + }); + if (result != 0 && this.chunk != null) { + this.chunk.retain(); + } + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + return this; + } + + @Override + public boolean release() { + int result = this.refCount.updateAndGet(c -> { + if (c != 0) { + return c - 1; + } + else { + throw new IllegalStateException("JettyDataBuffer already released: " + this); + } + }); + if (this.chunk != null) { + return this.chunk.release(); + } + else { + return result == 0; + } + } + + @Override + public DataBufferFactory factory() { + return this.bufferFactory; + } + + // delegation + + @Override + public int indexOf(IntPredicate predicate, int fromIndex) { + return this.delegate.indexOf(predicate, fromIndex); + } + + @Override + public int lastIndexOf(IntPredicate predicate, int fromIndex) { + return this.delegate.lastIndexOf(predicate, fromIndex); + } + + @Override + public int readableByteCount() { + return this.delegate.readableByteCount(); + } + + @Override + public int writableByteCount() { + return this.delegate.writableByteCount(); + } + + @Override + public int capacity() { + return this.delegate.capacity(); + } + + @Override + @Deprecated + public DataBuffer capacity(int capacity) { + this.delegate.capacity(capacity); + return this; + } + + @Override + public DataBuffer ensureWritable(int capacity) { + this.delegate.ensureWritable(capacity); + return this; + } + + @Override + public int readPosition() { + return this.delegate.readPosition(); + } + + @Override + public DataBuffer readPosition(int readPosition) { + this.delegate.readPosition(readPosition); + return this; + } + + @Override + public int writePosition() { + return this.delegate.writePosition(); + } + + @Override + public DataBuffer writePosition(int writePosition) { + this.delegate.writePosition(writePosition); + return this; + } + + @Override + public byte getByte(int index) { + return this.delegate.getByte(index); + } + + @Override + public byte read() { + return this.delegate.read(); + } + + @Override + public DataBuffer read(byte[] destination) { + this.delegate.read(destination); + return this; + } + + @Override + public DataBuffer read(byte[] destination, int offset, int length) { + this.delegate.read(destination, offset, length); + return this; + } + + @Override + public DataBuffer write(byte b) { + this.delegate.write(b); + return this; + } + + @Override + public DataBuffer write(byte[] source) { + this.delegate.write(source); + return this; + } + + @Override + public DataBuffer write(byte[] source, int offset, int length) { + this.delegate.write(source, offset, length); + return this; + } + + @Override + public DataBuffer write(DataBuffer... buffers) { + this.delegate.write(buffers); + return this; + } + + @Override + public DataBuffer write(ByteBuffer... buffers) { + this.delegate.write(buffers); + return this; + } + + @Override + @Deprecated + public DataBuffer slice(int index, int length) { + DefaultDataBuffer delegateSlice = this.delegate.slice(index, length); + if (this.chunk != null) { + this.chunk.retain(); + return new JettyDataBuffer(this.bufferFactory, delegateSlice, this.chunk); + } + else { + return new JettyDataBuffer(this.bufferFactory, delegateSlice); + } + } + + @Override + public DataBuffer split(int index) { + DefaultDataBuffer delegateSplit = this.delegate.split(index); + if (this.chunk != null) { + this.chunk.retain(); + return new JettyDataBuffer(this.bufferFactory, delegateSplit, this.chunk); + } + else { + return new JettyDataBuffer(this.bufferFactory, delegateSplit); + } + } + + @Override + @Deprecated + public ByteBuffer asByteBuffer() { + return this.delegate.asByteBuffer(); + } + + @Override + @Deprecated + public ByteBuffer asByteBuffer(int index, int length) { + return this.delegate.asByteBuffer(index, length); + } + + @Override + @Deprecated + public ByteBuffer toByteBuffer(int index, int length) { + return this.delegate.toByteBuffer(index, length); + } + + @Override + public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + this.delegate.toByteBuffer(srcPos, dest, destPos, length); + } + + @Override + public ByteBufferIterator readableByteBuffers() { + ByteBufferIterator delegateIterator = this.delegate.readableByteBuffers(); + if (this.chunk != null) { + return new JettyByteBufferIterator(delegateIterator, this.chunk); + } + else { + return delegateIterator; + } + } + + @Override + public ByteBufferIterator writableByteBuffers() { + ByteBufferIterator delegateIterator = this.delegate.writableByteBuffers(); + if (this.chunk != null) { + return new JettyByteBufferIterator(delegateIterator, this.chunk); + } + else { + return delegateIterator; + } + } + + @Override + public String toString(int index, int length, Charset charset) { + return this.delegate.toString(index, length, charset); + } + + + private static final class JettyByteBufferIterator implements ByteBufferIterator { + + private final ByteBufferIterator delegate; + + private final Content.Chunk chunk; + + + public JettyByteBufferIterator(ByteBufferIterator delegate, Content.Chunk chunk) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(chunk, "Chunk must not be null"); + + this.delegate = delegate; + this.chunk = chunk; + this.chunk.retain(); + } + + + @Override + public void close() { + this.delegate.close(); + this.chunk.release(); + } + + @Override + public boolean hasNext() { + return this.delegate.hasNext(); + } + + @Override + public ByteBuffer next() { + return this.delegate.next(); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java new file mode 100644 index 000000000000..7034a60f2a22 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBufferFactory.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io.buffer; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.eclipse.jetty.io.Content; + +/** + * Implementation of the {@code DataBufferFactory} interface that creates + * {@link JettyDataBuffer} instances. + * + * @author Arjen Poutsma + * @since 6.2 + */ +public class JettyDataBufferFactory implements DataBufferFactory { + + private final DefaultDataBufferFactory delegate; + + + /** + * Creates a new {@code JettyDataBufferFactory} with default settings. + */ + public JettyDataBufferFactory() { + this(false); + } + + /** + * Creates a new {@code JettyDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public JettyDataBufferFactory(boolean preferDirect) { + this(preferDirect, DefaultDataBufferFactory.DEFAULT_INITIAL_CAPACITY); + } + + /** + * Creates a new {@code JettyDataBufferFactory}, indicating whether direct + * buffers should be created by {@link #allocateBuffer()} and + * {@link #allocateBuffer(int)}, and what the capacity is to be used for + * {@link #allocateBuffer()}. + * @param preferDirect {@code true} if direct buffers are to be preferred; + * {@code false} otherwise + */ + public JettyDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { + this.delegate = new DefaultDataBufferFactory(preferDirect, defaultInitialCapacity); + } + + + @Override + @Deprecated + public JettyDataBuffer allocateBuffer() { + DefaultDataBuffer delegate = this.delegate.allocateBuffer(); + return new JettyDataBuffer(this, delegate); + } + + @Override + public JettyDataBuffer allocateBuffer(int initialCapacity) { + DefaultDataBuffer delegate = this.delegate.allocateBuffer(initialCapacity); + return new JettyDataBuffer(this, delegate); + } + + @Override + public JettyDataBuffer wrap(ByteBuffer byteBuffer) { + DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer); + return new JettyDataBuffer(this, delegate); + } + + @Override + public JettyDataBuffer wrap(byte[] bytes) { + DefaultDataBuffer delegate = this.delegate.wrap(bytes); + return new JettyDataBuffer(this, delegate); + } + + public JettyDataBuffer wrap(Content.Chunk chunk) { + ByteBuffer byteBuffer = chunk.getByteBuffer(); + DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer); + return new JettyDataBuffer(this, delegate, chunk); + } + + @Override + public JettyDataBuffer join(List dataBuffers) { + DefaultDataBuffer delegate = this.delegate.join(dataBuffers); + return new JettyDataBuffer(this, delegate); + } + + @Override + public boolean isDirect() { + return this.delegate.isDirect(); + } +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java new file mode 100644 index 000000000000..c41f83c21dc1 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.io.buffer; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.Content; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * @author Arjen Poutsma + */ +public class JettyDataBufferTests { + + private final JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory(); + + @Test + void releaseRetainChunk() { + ByteBuffer buffer = ByteBuffer.allocate(3); + Content.Chunk mockChunk = mock(); + given(mockChunk.getByteBuffer()).willReturn(buffer); + given(mockChunk.release()).willReturn(false, false, true); + + + + JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(mockChunk); + dataBuffer.retain(); + dataBuffer.retain(); + assertThat(dataBuffer.release()).isFalse(); + assertThat(dataBuffer.release()).isFalse(); + assertThat(dataBuffer.release()).isTrue(); + + assertThatIllegalStateException().isThrownBy(dataBuffer::release); + + then(mockChunk).should(times(2)).retain(); + then(mockChunk).should(times(3)).release(); + } +} diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java index 5f353ad5967e..87843ca6b678 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/PooledDataBufferTests.java @@ -69,6 +69,15 @@ public DataBufferFactory createDataBufferFactory() { } } + @Nested + class Jetty implements PooledDataBufferTestingTrait { + + @Override + public DataBufferFactory createDataBufferFactory() { + return new JettyDataBufferFactory(); + } + } + interface PooledDataBufferTestingTrait { @@ -82,10 +91,14 @@ default PooledDataBuffer createDataBuffer(int capacity) { default void retainAndRelease() { PooledDataBuffer buffer = createDataBuffer(1); buffer.write((byte) 'a'); + assertThat(buffer.isAllocated()).isTrue(); buffer.retain(); + assertThat(buffer.isAllocated()).isTrue(); assertThat(buffer.release()).isFalse(); + assertThat(buffer.isAllocated()).isTrue(); assertThat(buffer.release()).isTrue(); + assertThat(buffer.isAllocated()).isFalse(); } @Test diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index a2895f6ded57..071a161e9096 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -17,24 +17,16 @@ package org.springframework.http.client.reactive; import java.net.URI; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.IntPredicate; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.Request; -import org.eclipse.jetty.io.Content; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; -import org.springframework.core.io.buffer.TouchableDataBuffer; +import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -50,7 +42,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private final HttpClient httpClient; - private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory(); /** @@ -103,7 +95,7 @@ public JettyClientHttpConnector(JettyResourceFactory resourceFactory, @Nullable /** * Set the buffer factory to use. */ - public void setBufferFactory(DataBufferFactory bufferFactory) { + public void setBufferFactory(JettyDataBufferFactory bufferFactory) { this.bufferFactory = bufferFactory; } @@ -134,289 +126,9 @@ public Mono connect(HttpMethod method, URI uri, private Mono execute(JettyClientHttpRequest request) { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { - Flux content = Flux.from(chunkPublisher).map(this::toDataBuffer); + Flux content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap); return Mono.just(new JettyClientHttpResponse(reactiveResponse, content)); })); } - private DataBuffer toDataBuffer(Content.Chunk chunk) { - DataBuffer delegate = this.bufferFactory.wrap(chunk.getByteBuffer()); - return new JettyDataBuffer(delegate, chunk); - } - - - private static final class JettyDataBuffer implements PooledDataBuffer { - - private final DataBuffer delegate; - - private final Content.Chunk chunk; - - private final AtomicInteger refCount = new AtomicInteger(1); - - - public JettyDataBuffer(DataBuffer delegate, Content.Chunk chunk) { - Assert.notNull(delegate, "Delegate must not be null"); - Assert.notNull(chunk, "Chunk must not be null"); - - this.delegate = delegate; - this.chunk = chunk; - } - - @Override - public boolean isAllocated() { - return this.refCount.get() > 0; - } - - @Override - public PooledDataBuffer retain() { - if (this.delegate instanceof PooledDataBuffer pooledDelegate) { - pooledDelegate.retain(); - } - this.chunk.retain(); - this.refCount.getAndUpdate(c -> { - if (c != 0) { - return c + 1; - } - else { - return 0; - } - }); - return this; - } - - @Override - public boolean release() { - if (this.delegate instanceof PooledDataBuffer pooledDelegate) { - pooledDelegate.release(); - } - this.chunk.release(); - int refCount = this.refCount.updateAndGet(c -> { - if (c != 0) { - return c - 1; - } - else { - throw new IllegalStateException("already released " + this); - } - }); - return refCount == 0; - } - - @Override - public PooledDataBuffer touch(Object hint) { - if (this.delegate instanceof TouchableDataBuffer touchableDelegate) { - touchableDelegate.touch(hint); - } - return this; - } - - // delegation - - @Override - public DataBufferFactory factory() { - return this.delegate.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return this.delegate.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return this.delegate.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return this.delegate.readableByteCount(); - } - - @Override - public int writableByteCount() { - return this.delegate.writableByteCount(); - } - - @Override - public int capacity() { - return this.delegate.capacity(); - } - - @Override - @Deprecated - public DataBuffer capacity(int capacity) { - this.delegate.capacity(capacity); - return this; - } - - @Override - public DataBuffer ensureWritable(int capacity) { - this.delegate.ensureWritable(capacity); - return this; - } - - @Override - public int readPosition() { - return this.delegate.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - this.delegate.readPosition(readPosition); - return this; - } - - @Override - public int writePosition() { - return this.delegate.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - this.delegate.writePosition(writePosition); - return this; - } - - @Override - public byte getByte(int index) { - return this.delegate.getByte(index); - } - - @Override - public byte read() { - return this.delegate.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - this.delegate.read(destination); - return this; - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - this.delegate.read(destination, offset, length); - return this; - } - - @Override - public DataBuffer write(byte b) { - this.delegate.write(b); - return this; - } - - @Override - public DataBuffer write(byte[] source) { - this.delegate.write(source); - return this; - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - this.delegate.write(source, offset, length); - return this; - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - this.delegate.write(buffers); - return this; - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - this.delegate.write(buffers); - return this; - } - - @Override - @Deprecated - public DataBuffer slice(int index, int length) { - DataBuffer delegateSlice = this.delegate.slice(index, length); - this.chunk.retain(); - return new JettyDataBuffer(delegateSlice, this.chunk); - } - - @Override - public DataBuffer split(int index) { - DataBuffer delegateSplit = this.delegate.split(index); - this.chunk.retain(); - return new JettyDataBuffer(delegateSplit, this.chunk); - } - - @Override - @Deprecated - public ByteBuffer asByteBuffer() { - return this.delegate.asByteBuffer(); - } - - @Override - @Deprecated - public ByteBuffer asByteBuffer(int index, int length) { - return this.delegate.asByteBuffer(index, length); - } - - @Override - @Deprecated - public ByteBuffer toByteBuffer(int index, int length) { - return this.delegate.toByteBuffer(index, length); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - this.delegate.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - ByteBufferIterator delegateIterator = this.delegate.readableByteBuffers(); - return new JettyByteBufferIterator(delegateIterator, this.chunk); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - ByteBufferIterator delegateIterator = this.delegate.writableByteBuffers(); - return new JettyByteBufferIterator(delegateIterator, this.chunk); - } - - @Override - public String toString(int index, int length, Charset charset) { - return this.delegate.toString(index, length, charset); - } - - - private static final class JettyByteBufferIterator implements ByteBufferIterator { - - private final ByteBufferIterator delegate; - - private final Content.Chunk chunk; - - - public JettyByteBufferIterator(ByteBufferIterator delegate, Content.Chunk chunk) { - Assert.notNull(delegate, "Delegate must not be null"); - Assert.notNull(chunk, "Chunk must not be null"); - - this.delegate = delegate; - this.chunk = chunk; - this.chunk.retain(); - } - - - @Override - public void close() { - this.delegate.close(); - this.chunk.release(); - } - - @Override - public boolean hasNext() { - return this.delegate.hasNext(); - } - - @Override - public ByteBuffer next() { - return this.delegate.next(); - } - } - } - } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java index 8a7b8e31c13e..08994a11f624 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java @@ -21,8 +21,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.util.Assert; /** @@ -37,14 +36,14 @@ public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { private final HttpHandler httpHandler; - private DataBufferFactory dataBufferFactory = DefaultDataBufferFactory.sharedInstance; + private JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory(); public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { this.httpHandler = httpHandler; } - public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { + public void setDataBufferFactory(JettyDataBufferFactory dataBufferFactory) { Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); this.dataBufferFactory = dataBufferFactory; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index a0f4ffa2c058..05a66331c4fb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -28,7 +28,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -47,12 +47,12 @@ */ class JettyCoreServerHttpRequest extends AbstractServerHttpRequest { - private final DataBufferFactory dataBufferFactory; + private final JettyDataBufferFactory dataBufferFactory; private final Request request; - public JettyCoreServerHttpRequest(Request request, DataBufferFactory dataBufferFactory) { + public JettyCoreServerHttpRequest(Request request, JettyDataBufferFactory dataBufferFactory) { super(HttpMethod.valueOf(request.getMethod()), request.getHttpURI().toURI(), request.getContext().getContextPath(), @@ -111,7 +111,8 @@ public Flux getBody() { } private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { - return new JettyRetainedDataBuffer(this.dataBufferFactory, chunk); + chunk.retain(); + return this.dataBufferFactory.wrap(chunk); } @SuppressWarnings("unchecked") diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index fabbec9d19ee..b55d6a60984d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -43,8 +43,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; @@ -63,7 +63,7 @@ class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements private final Response response; - public JettyCoreServerHttpResponse(Response response, DataBufferFactory dataBufferFactory) { + public JettyCoreServerHttpResponse(Response response, JettyDataBufferFactory dataBufferFactory) { super(dataBufferFactory, new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); this.response = response; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java deleted file mode 100644 index b35fabf017c3..000000000000 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.http.server.reactive; - -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.IntPredicate; - -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.Retainable; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; - -/** - * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer}. - * - * @author Greg Wilkins - * @author Lachlan Roberts - * @since 6.1.4 - */ -public class JettyRetainedDataBuffer implements PooledDataBuffer { - - private final Content.Chunk chunk; - - private final DataBuffer dataBuffer; - - private final AtomicInteger allocated = new AtomicInteger(1); - - - public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { - this.chunk = chunk; - // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); // TODO this copy avoids multipart bugs - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? - this.chunk.retain(); - } - - @Override - public boolean isAllocated() { - return this.allocated.get() >= 1; - } - - @Override - public PooledDataBuffer retain() { - if (this.allocated.updateAndGet(c -> c >= 1 ? c + 1 : c) < 1) { - throw new IllegalStateException("released"); - } - return this; - } - - @Override - public PooledDataBuffer touch(Object hint) { - return this; - } - - @Override - public boolean release() { - if (this.allocated.decrementAndGet() == 0) { - this.chunk.release(); - return true; - } - return false; - } - - @Override - public DataBufferFactory factory() { - return this.dataBuffer.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return this.dataBuffer.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return this.dataBuffer.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return this.dataBuffer.readableByteCount(); - } - - @Override - public int writableByteCount() { - return this.dataBuffer.writableByteCount(); - } - - @Override - public int capacity() { - return this.dataBuffer.capacity(); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer capacity(int capacity) { - return this.dataBuffer.capacity(capacity); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer ensureCapacity(int capacity) { - return this.dataBuffer.ensureCapacity(capacity); - } - - @Override - public DataBuffer ensureWritable(int capacity) { - return this.dataBuffer.ensureWritable(capacity); - } - - @Override - public int readPosition() { - return this.dataBuffer.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - return this.dataBuffer.readPosition(readPosition); - } - - @Override - public int writePosition() { - return this.dataBuffer.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - return this.dataBuffer.writePosition(writePosition); - } - - @Override - public byte getByte(int index) { - return this.dataBuffer.getByte(index); - } - - @Override - public byte read() { - return this.dataBuffer.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - return this.dataBuffer.read(destination); - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - return this.dataBuffer.read(destination, offset, length); - } - - @Override - public DataBuffer write(byte b) { - return this.dataBuffer.write(b); - } - - @Override - public DataBuffer write(byte[] source) { - return this.dataBuffer.write(source); - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - return this.dataBuffer.write(source, offset, length); - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - return this.dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - return this.dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - return this.dataBuffer.write(charSequence, charset); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer slice(int index, int length) { - return this.dataBuffer.slice(index, length); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer retainedSlice(int index, int length) { - return this.dataBuffer.retainedSlice(index, length); - } - - @Override - public DataBuffer split(int index) { - return this.dataBuffer.split(index); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer() { - return this.dataBuffer.asByteBuffer(); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer(int index, int length) { - return this.dataBuffer.asByteBuffer(index, length); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer() { - return this.dataBuffer.toByteBuffer(); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer(int index, int length) { - return this.dataBuffer.toByteBuffer(index, length); - } - - @Override - public void toByteBuffer(ByteBuffer dest) { - this.dataBuffer.toByteBuffer(dest); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - this.dataBuffer.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return this.dataBuffer.readableByteBuffers(); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return this.dataBuffer.writableByteBuffers(); - } - - @Override - public InputStream asInputStream() { - return this.dataBuffer.asInputStream(); - } - - @Override - public InputStream asInputStream(boolean releaseOnClose) { - return this.dataBuffer.asInputStream(releaseOnClose); - } - - @Override - public OutputStream asOutputStream() { - return this.dataBuffer.asOutputStream(); - } - - @Override - public String toString(Charset charset) { - return this.dataBuffer.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - return this.dataBuffer.toString(index, length, charset); - } -} From 6c48a832f3771356e505237a34cf53025fe75ea9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 5 Jun 2024 14:42:36 +0200 Subject: [PATCH 137/146] Polish JettyDataBuffer --- .../springframework/core/io/buffer/JettyDataBuffer.java | 1 + .../core/io/buffer/JettyDataBufferTests.java | 2 +- .../http/server/reactive/JettyCoreServerHttpRequest.java | 8 ++------ 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java index b272174d2999..b03decb4527c 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java @@ -55,6 +55,7 @@ public final class JettyDataBuffer implements PooledDataBuffer { this.bufferFactory = bufferFactory; this.delegate = delegate; this.chunk = chunk; + this.chunk.retain(); } JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) { diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java index c41f83c21dc1..456338c10917 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/JettyDataBufferTests.java @@ -53,7 +53,7 @@ void releaseRetainChunk() { assertThatIllegalStateException().isThrownBy(dataBuffer::release); - then(mockChunk).should(times(2)).retain(); + then(mockChunk).should(times(3)).retain(); then(mockChunk).should(times(3)).release(); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java index 05a66331c4fb..cef5e955f4f8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java @@ -107,12 +107,8 @@ public InetSocketAddress getRemoteAddress() { public Flux getBody() { // We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and // then wrapped as a Flux. - return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))).map(this::chunkToDataBuffer); - } - - private DataBuffer chunkToDataBuffer(Content.Chunk chunk) { - chunk.retain(); - return this.dataBufferFactory.wrap(chunk); + return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) + .map(this.dataBufferFactory::wrap); } @SuppressWarnings("unchecked") From feef4a7e758bf272b80b0c6abc913e351c7020f6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 6 Jun 2024 10:38:02 +0200 Subject: [PATCH 138/146] Polishing --- .../core/io/buffer/JettyDataBuffer.java | 16 ++++++ .../adapter/JettyWebSocketHandlerAdapter.java | 49 +++++++------------ .../socket/adapter/JettyWebSocketSession.java | 4 ++ .../jetty/JettyWebSocketHandlerAdapter.java | 22 ++++++--- 4 files changed, 52 insertions(+), 39 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java index b03decb4527c..f2eb53063197 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/JettyDataBuffer.java @@ -305,6 +305,22 @@ public String toString(int index, int length, Charset charset) { return this.delegate.toString(index, length, charset); } + @Override + public int hashCode() { + return this.delegate.hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof JettyDataBuffer other && + this.delegate.equals(other.delegate)); + } + + @Override + public String toString() { + return String.format("JettyDataBuffer (r: %d, w: %d, c: %d)", + readPosition(), writePosition(), capacity()); + } private static final class JettyByteBufferIterator implements ByteBufferIterator { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 584eddfe1cba..1e5ee65e0d75 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -19,15 +19,12 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.Objects; import java.util.function.Function; import java.util.function.IntPredicate; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Session; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.CloseableDataBuffer; import org.springframework.core.io.buffer.DataBuffer; @@ -47,8 +44,8 @@ * @author Rossen Stoyanchev * @since 5.0 */ -@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { + private final WebSocketHandler delegateHandler; private final Function sessionFactory; @@ -56,6 +53,7 @@ public class JettyWebSocketHandlerAdapter implements Session.Listener { @Nullable private JettyWebSocketSession delegateSession; + public JettyWebSocketHandlerAdapter(WebSocketHandler handler, Function sessionFactory) { @@ -67,32 +65,16 @@ public JettyWebSocketHandlerAdapter(WebSocketHandler handler, @Override public void onWebSocketOpen(Session session) { - this.delegateSession = Objects.requireNonNull(this.sessionFactory.apply(session)); - this.delegateHandler.handle(this.delegateSession) - .subscribe(new Subscriber<>() { - @Override - public void onSubscribe(Subscription s) { - s.request(Long.MAX_VALUE); - } - - @Override - public void onNext(Void unused) { - } - - @Override - public void onError(Throwable t) { - delegateSession.onHandlerError(t); - } - - @Override - public void onComplete() { - delegateSession.onHandleComplete(); - } - }); + JettyWebSocketSession delegateSession = this.sessionFactory.apply(session); + this.delegateSession = delegateSession; + this.delegateHandler.handle(delegateSession) + .checkpoint(session.getUpgradeRequest().getRequestURI() + " [JettyWebSocketHandlerAdapter]") + .subscribe(unused -> {}, delegateSession::onHandlerError, delegateSession::onHandleComplete); } @Override public void onWebSocketText(String message) { + Assert.state(this.delegateSession != null, "No delegate session available"); byte[] bytes = message.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = this.delegateSession.bufferFactory().wrap(bytes); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.TEXT, buffer); @@ -101,14 +83,16 @@ public void onWebSocketText(String message) { @Override public void onWebSocketBinary(ByteBuffer byteBuffer, Callback callback) { + Assert.state(this.delegateSession != null, "No delegate session available"); DataBuffer buffer = this.delegateSession.bufferFactory().wrap(byteBuffer); - buffer = new JettyDataBuffer(buffer, callback); + buffer = new JettyCallbackDataBuffer(buffer, callback); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.BINARY, buffer); this.delegateSession.handleMessage(webSocketMessage); } @Override public void onWebSocketPong(ByteBuffer payload) { + Assert.state(this.delegateSession != null, "No delegate session available"); DataBuffer buffer = this.delegateSession.bufferFactory().wrap(BufferUtil.copy(payload)); WebSocketMessage webSocketMessage = new WebSocketMessage(Type.PONG, buffer); this.delegateSession.handleMessage(webSocketMessage); @@ -116,22 +100,25 @@ public void onWebSocketPong(ByteBuffer payload) { @Override public void onWebSocketClose(int statusCode, String reason) { + Assert.state(this.delegateSession != null, "No delegate session available"); this.delegateSession.handleClose(CloseStatus.create(statusCode, reason)); } @Override public void onWebSocketError(Throwable cause) { + Assert.state(this.delegateSession != null, "No delegate session available"); this.delegateSession.handleError(cause); } - private static final class JettyDataBuffer implements CloseableDataBuffer { + private static final class JettyCallbackDataBuffer implements CloseableDataBuffer { private final DataBuffer delegate; private final Callback callback; - public JettyDataBuffer(DataBuffer delegate, Callback callback) { + + public JettyCallbackDataBuffer(DataBuffer delegate, Callback callback) { Assert.notNull(delegate, "'delegate` must not be null"); Assert.notNull(callback, "Callback must not be null"); this.delegate = delegate; @@ -266,13 +253,13 @@ public DataBuffer write(ByteBuffer... buffers) { @Deprecated public DataBuffer slice(int index, int length) { DataBuffer delegateSlice = this.delegate.slice(index, length); - return new JettyDataBuffer(delegateSlice, this.callback); + return new JettyCallbackDataBuffer(delegateSlice, this.callback); } @Override public DataBuffer split(int index) { DataBuffer delegateSplit = this.delegate.split(index); - return new JettyDataBuffer(delegateSplit, this.callback); + return new JettyCallbackDataBuffer(delegateSplit, this.callback); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 9521b83ede7a..3179dd51e2c4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -53,9 +53,13 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; + private final Sinks.One closeStatusSink = Sinks.one(); + private final Lock lock = new ReentrantLock(); + private long requested = 0; + private boolean awaitingMessage = false; @Nullable diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java index 3322fc49f566..f57dff500877 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java @@ -39,16 +39,18 @@ * @author Rossen Stoyanchev * @since 4.0 */ -@SuppressWarnings("NullAway") public class JettyWebSocketHandlerAdapter implements Session.Listener { + private static final Log logger = LogFactory.getLog(JettyWebSocketHandlerAdapter.class); private final WebSocketHandler webSocketHandler; + private final JettyWebSocketSession wsSession; @Nullable private Session nativeSession; + public JettyWebSocketHandlerAdapter(WebSocketHandler webSocketHandler, JettyWebSocketSession wsSession) { Assert.notNull(webSocketHandler, "WebSocketHandler must not be null"); Assert.notNull(wsSession, "WebSocketSession must not be null"); @@ -71,6 +73,7 @@ public void onWebSocketOpen(Session session) { @Override public void onWebSocketText(String payload) { + Assert.state(this.nativeSession != null, "No native session available"); TextMessage message = new TextMessage(payload); try { this.webSocketHandler.handleMessage(this.wsSession, message); @@ -83,6 +86,7 @@ public void onWebSocketText(String payload) { @Override public void onWebSocketBinary(ByteBuffer payload, Callback callback) { + Assert.state(this.nativeSession != null, "No native session available"); BinaryMessage message = new BinaryMessage(BufferUtil.copy(payload), true); callback.succeed(); try { @@ -96,6 +100,7 @@ public void onWebSocketBinary(ByteBuffer payload, Callback callback) { @Override public void onWebSocketPong(ByteBuffer payload) { + Assert.state(this.nativeSession != null, "No native session available"); PongMessage message = new PongMessage(BufferUtil.copy(payload)); try { this.webSocketHandler.handleMessage(this.wsSession, message); @@ -132,13 +137,14 @@ public void onWebSocketError(Throwable cause) { } private void tryCloseWithError(Throwable t) { - - if (this.nativeSession.isOpen()) { - ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); - } - else { - // Session might be O-SHUT waiting for response close frame, so abort to close the connection. - this.nativeSession.disconnect(); + if (this.nativeSession != null) { + if (this.nativeSession.isOpen()) { + ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.wsSession, t, logger); + } + else { + // Session might be O-SHUT waiting for response close frame, so abort to close the connection. + this.nativeSession.disconnect(); + } } } } From d00ed1e4ef7d8b7040155160f43e340d1f03514e Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 6 Jun 2024 12:23:41 +0200 Subject: [PATCH 139/146] Polishing --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 3179dd51e2c4..74c698cc1c21 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -35,6 +35,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.HandshakeInfo; @@ -49,7 +50,6 @@ * @author Rossen Stoyanchev * @since 5.0 */ -@SuppressWarnings("NullAway") public class JettyWebSocketSession extends AbstractWebSocketSession { private final Flux flux; @@ -102,6 +102,7 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact } void handleMessage(WebSocketMessage message) { + Assert.state(this.sink != null, "No sink available"); this.sink.next(message); boolean demand = false; @@ -131,7 +132,9 @@ void handleError(Throwable ex) { void handleClose(CloseStatus closeStatus) { this.closeStatusSink.tryEmitValue(closeStatus); - this.sink.complete(); + if (this.sink != null) { + this.sink.complete(); + } } void onHandlerError(Throwable error) { From 08ce47f540dfcbcd7d428e275e3f65eefa9aa53b Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 12 Jun 2024 10:55:10 +1000 Subject: [PATCH 140/146] Merge remote-tracking branch 'poutsma/gh-32097' into JettyCoreHttpHandlerAdapter # Conflicts: # spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java # spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreHttpHandlerAdapter.java # spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpRequest.java # spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java # spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java # spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java # spring-websocket/src/main/java/org/springframework/web/socket/adapter/jetty/JettyWebSocketHandlerAdapter.java --- spring-jcl/spring-jcl.gradle | 2 - .../reactive/AbstractServerHttpRequest.java | 9 +- .../reactive/AbstractServerHttpResponse.java | 8 + .../reactive/JettyRetainedDataBuffer.java | 285 ------------------ .../server/reactive/ServerHttpRequest.java | 10 - .../reactive/ServerHttpRequestDecorator.java | 17 +- .../server/reactive/ServerHttpResponse.java | 9 - .../reactive/ServerHttpResponseDecorator.java | 16 +- .../result/view/ZeroDemandResponse.java | 4 - 9 files changed, 30 insertions(+), 330 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle index 560d85324cb0..d609737b2551 100644 --- a/spring-jcl/spring-jcl.gradle +++ b/spring-jcl/spring-jcl.gradle @@ -3,6 +3,4 @@ description = "Spring Commons Logging Bridge" dependencies { optional("org.apache.logging.log4j:log4j-api") optional("org.slf4j:slf4j-api") - optional("biz.aQute.bnd:biz.aQute.bnd.annotation:6.3.1") - optional("org.osgi:osgi.annotation:8.1.0") } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 9e4f1547e171..829a2202a814 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -42,7 +42,7 @@ */ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { - static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); + private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); private final URI uri; @@ -203,6 +203,13 @@ public SslInfo getSslInfo() { @Nullable protected abstract SslInfo initSslInfo(); + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + public abstract T getNativeRequest(); + /** * For internal use in logging at the HTTP adapter layer. * @since 5.1 diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 82731c218d90..7a06126ce85d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -155,6 +155,14 @@ public void addCookie(ResponseCookie cookie) { } } + /** + * Return the underlying server response. + *

Note: This is exposed mainly for internal framework + * use such as WebSocket upgrades in the spring-webflux module. + */ + public abstract T getNativeResponse(); + + @Override public void beforeCommit(Supplier> action) { this.commitActions.add(action); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java deleted file mode 100644 index b35fabf017c3..000000000000 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyRetainedDataBuffer.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.http.server.reactive; - -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.IntPredicate; - -import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.Retainable; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.PooledDataBuffer; - -/** - * Adapt an Eclipse Jetty {@link Retainable} to a {@link PooledDataBuffer}. - * - * @author Greg Wilkins - * @author Lachlan Roberts - * @since 6.1.4 - */ -public class JettyRetainedDataBuffer implements PooledDataBuffer { - - private final Content.Chunk chunk; - - private final DataBuffer dataBuffer; - - private final AtomicInteger allocated = new AtomicInteger(1); - - - public JettyRetainedDataBuffer(DataBufferFactory dataBufferFactory, Content.Chunk chunk) { - this.chunk = chunk; - // this.dataBuffer = dataBufferFactory.wrap(BufferUtil.copy(chunk.getByteBuffer())); // TODO this copy avoids multipart bugs - this.dataBuffer = dataBufferFactory.wrap(chunk.getByteBuffer()); // TODO avoid double slice? - this.chunk.retain(); - } - - @Override - public boolean isAllocated() { - return this.allocated.get() >= 1; - } - - @Override - public PooledDataBuffer retain() { - if (this.allocated.updateAndGet(c -> c >= 1 ? c + 1 : c) < 1) { - throw new IllegalStateException("released"); - } - return this; - } - - @Override - public PooledDataBuffer touch(Object hint) { - return this; - } - - @Override - public boolean release() { - if (this.allocated.decrementAndGet() == 0) { - this.chunk.release(); - return true; - } - return false; - } - - @Override - public DataBufferFactory factory() { - return this.dataBuffer.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return this.dataBuffer.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return this.dataBuffer.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return this.dataBuffer.readableByteCount(); - } - - @Override - public int writableByteCount() { - return this.dataBuffer.writableByteCount(); - } - - @Override - public int capacity() { - return this.dataBuffer.capacity(); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer capacity(int capacity) { - return this.dataBuffer.capacity(capacity); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer ensureCapacity(int capacity) { - return this.dataBuffer.ensureCapacity(capacity); - } - - @Override - public DataBuffer ensureWritable(int capacity) { - return this.dataBuffer.ensureWritable(capacity); - } - - @Override - public int readPosition() { - return this.dataBuffer.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - return this.dataBuffer.readPosition(readPosition); - } - - @Override - public int writePosition() { - return this.dataBuffer.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - return this.dataBuffer.writePosition(writePosition); - } - - @Override - public byte getByte(int index) { - return this.dataBuffer.getByte(index); - } - - @Override - public byte read() { - return this.dataBuffer.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - return this.dataBuffer.read(destination); - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - return this.dataBuffer.read(destination, offset, length); - } - - @Override - public DataBuffer write(byte b) { - return this.dataBuffer.write(b); - } - - @Override - public DataBuffer write(byte[] source) { - return this.dataBuffer.write(source); - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - return this.dataBuffer.write(source, offset, length); - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - return this.dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - return this.dataBuffer.write(buffers); - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - return this.dataBuffer.write(charSequence, charset); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer slice(int index, int length) { - return this.dataBuffer.slice(index, length); - } - - @Override - @Deprecated(since = "6.0") - public DataBuffer retainedSlice(int index, int length) { - return this.dataBuffer.retainedSlice(index, length); - } - - @Override - public DataBuffer split(int index) { - return this.dataBuffer.split(index); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer() { - return this.dataBuffer.asByteBuffer(); - } - - @Override - @Deprecated(since = "6.0") - public ByteBuffer asByteBuffer(int index, int length) { - return this.dataBuffer.asByteBuffer(index, length); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer() { - return this.dataBuffer.toByteBuffer(); - } - - @Override - @Deprecated(since = "6.0.5") - public ByteBuffer toByteBuffer(int index, int length) { - return this.dataBuffer.toByteBuffer(index, length); - } - - @Override - public void toByteBuffer(ByteBuffer dest) { - this.dataBuffer.toByteBuffer(dest); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - this.dataBuffer.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return this.dataBuffer.readableByteBuffers(); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return this.dataBuffer.writableByteBuffers(); - } - - @Override - public InputStream asInputStream() { - return this.dataBuffer.asInputStream(); - } - - @Override - public InputStream asInputStream(boolean releaseOnClose) { - return this.dataBuffer.asInputStream(releaseOnClose); - } - - @Override - public OutputStream asOutputStream() { - return this.dataBuffer.asOutputStream(); - } - - @Override - public String toString(Charset charset) { - return this.dataBuffer.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - return this.dataBuffer.toString(index, length, charset); - } -} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 41814c35aeb0..55f5fa2c654b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -97,16 +97,6 @@ default SslInfo getSslInfo() { return null; } - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - @Nullable - default T getNativeRequest() { - return null; - } - /** * Return a builder to mutate properties of this request by wrapping it * with {@link ServerHttpRequestDecorator} and returning either mutated diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 46100e7edbbc..fc6143bfdaf0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -108,12 +108,6 @@ public SslInfo getSslInfo() { return getDelegate().getSslInfo(); } - @Override - @Nullable - public T getNativeRequest() { - return this.delegate.getNativeRequest(); - } - @Override public Flux getBody() { return getDelegate().getBody(); @@ -129,13 +123,16 @@ public Flux getBody() { * @since 5.3.3 */ public static T getNativeRequest(ServerHttpRequest request) { - T nativeRequest = request.getNativeRequest(); - if (nativeRequest == null) { + if (request instanceof AbstractServerHttpRequest abstractServerHttpRequest) { + return abstractServerHttpRequest.getNativeRequest(); + } + else if (request instanceof ServerHttpRequestDecorator serverHttpRequestDecorator) { + return getNativeRequest(serverHttpRequestDecorator.getDelegate()); + } + else { throw new IllegalArgumentException( "Can't find native request in " + request.getClass().getName()); } - - return nativeRequest; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java index 5ab1708957a7..7f97d04484a5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponse.java @@ -86,13 +86,4 @@ default Integer getRawStatusCode() { */ void addCookie(ResponseCookie cookie); - /** - * Return the underlying server response. - *

Note: This is exposed mainly for internal framework - * use such as WebSocket upgrades in the spring-webflux module. - */ - @Nullable - default T getNativeResponse() { - return null; - } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java index 3398a3416c0b..d6f7f85b953c 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpResponseDecorator.java @@ -123,11 +123,6 @@ public Mono setComplete() { return getDelegate().setComplete(); } - @Override - @Nullable - public T getNativeResponse() { - return getDelegate().getNativeResponse(); - } /** * Return the native response of the underlying server API, if possible, @@ -138,13 +133,16 @@ public T getNativeResponse() { * @since 5.3.3 */ public static T getNativeResponse(ServerHttpResponse response) { - T nativeResponse = response.getNativeResponse(); - if (nativeResponse == null) { + if (response instanceof AbstractServerHttpResponse abstractServerHttpResponse) { + return abstractServerHttpResponse.getNativeResponse(); + } + else if (response instanceof ServerHttpResponseDecorator serverHttpResponseDecorator) { + return getNativeResponse(serverHttpResponseDecorator.getDelegate()); + } + else { throw new IllegalArgumentException( "Can't find native response in " + response.getClass().getName()); } - - return nativeResponse; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java index c189651e433e..b543447e1e68 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ZeroDemandResponse.java @@ -116,10 +116,6 @@ public HttpHeaders getHeaders() { throw new UnsupportedOperationException(); } - @Override - public T getNativeResponse() { - return null; - } private static class ZeroDemandSubscriber extends BaseSubscriber { From 260da306ddc5dd0c701ceafc1edcf6d11e060db2 Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 12 Jun 2024 11:35:13 +1000 Subject: [PATCH 141/146] addExact --- .../web/reactive/socket/adapter/JettyWebSocketSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 74c698cc1c21..0b514623a094 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -83,7 +83,7 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact boolean demand = false; this.lock.lock(); try { - this.requested += n; + this.requested = Math.addExact(this.requested, n); if (!this.awaitingMessage && this.requested > 0) { this.requested--; this.awaitingMessage = true; From d4157574cf1fe74a0a258de4eed2008c8c2a595a Mon Sep 17 00:00:00 2001 From: gregw Date: Wed, 12 Jun 2024 12:07:06 +1000 Subject: [PATCH 142/146] doCommit not required --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index b55d6a60984d..4ba19b3a2894 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -159,7 +159,7 @@ protected void onCompleteFailure(Throwable cause) { } }.iterate(); - return doCommit(() -> Mono.fromFuture(callback)); + return Mono.fromFuture(callback); } @SuppressWarnings("unchecked") From 0b4b65f50b7853b7ffb13381f22e0561a24e374f Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Sat, 22 Jun 2024 15:32:38 +1000 Subject: [PATCH 143/146] PR #32097 - changes to address reviews for websocket Signed-off-by: Lachlan Roberts --- .../reactive/bootstrap/JettyCoreHttpServer.java | 6 ++---- .../socket/adapter/JettyWebSocketSession.java | 12 ++++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java index ba1a085ebcec..488e0e429ef9 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/bootstrap/JettyCoreHttpServer.java @@ -21,7 +21,7 @@ import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; +import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; @@ -51,9 +51,7 @@ protected void initServer() { this.jettyServer.addConnector(connector); this.jettyServer.setHandler(createHandlerAdapter()); - // TODO: We don't actually want the upgrade handler but this will create the WebSocketContainer. - // This requires a change in Jetty. - WebSocketUpgradeHandler.from(jettyServer); + ServerWebSocketContainer.ensure(jettyServer); } private JettyCoreHttpHandlerAdapter createHandlerAdapter() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 0b514623a094..35aeac20a808 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -84,8 +84,14 @@ public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFact this.lock.lock(); try { this.requested = Math.addExact(this.requested, n); + if (this.requested < 0L) { + this.requested = Long.MAX_VALUE; + } + if (!this.awaitingMessage && this.requested > 0) { - this.requested--; + if (this.requested != Long.MAX_VALUE) { + this.requested--; + } this.awaitingMessage = true; demand = true; } @@ -113,7 +119,9 @@ void handleMessage(WebSocketMessage message) { } this.awaitingMessage = false; if (this.requested > 0) { - this.requested--; + if (this.requested != Long.MAX_VALUE) { + this.requested--; + } this.awaitingMessage = true; demand = true; } From 5dd2538c675c301ae2745b94bbb48058f0d941ce Mon Sep 17 00:00:00 2001 From: gregw Date: Mon, 1 Jul 2024 13:48:50 +1000 Subject: [PATCH 144/146] doCommit not required --- framework-platform/framework-platform.gradle | 4 +- .../reactive/JettyCoreServerHttpResponse.java | 68 +------------------ 2 files changed, 4 insertions(+), 68 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 839a07cbdbff..a62040b80dfb 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -16,8 +16,8 @@ dependencies { api(platform("org.apache.groovy:groovy-bom:4.0.21")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.assertj:assertj-bom:3.26.0")) - api(platform("org.eclipse.jetty:jetty-bom:12.0.10")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.10")) + api(platform("org.eclipse.jetty:jetty-bom:12.0.11")) + api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.11")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0")) api(platform("org.junit:junit-bom:5.10.2")) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 4ba19b3a2894..74df330a2a45 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.RetainableByteBuffer; +import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; @@ -119,10 +120,7 @@ public Mono writeWith(Path file, long position, long count) { Callback.Completable callback = new Callback.Completable(); Mono mono = Mono.fromFuture(callback); try { - // The method can block, but it is not expected to do so for any significant time. - @SuppressWarnings("BlockingMethodInNonBlockingContext") - SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ); - new ContentWriterIteratingCallback(channel, position, count, this.response, callback).iterate(); + Content.copy(Content.Source.from(null, file, position, count), this.response, callback); } catch (Throwable th) { callback.failed(th); @@ -244,66 +242,4 @@ public Map getAttributes() { return Collections.emptyMap(); } } - - private static class ContentWriterIteratingCallback extends IteratingCallback { - private final SeekableByteChannel source; - - private final Content.Sink sink; - - private final Callback callback; - - private final RetainableByteBuffer buffer; - - private final long length; - - private long totalRead = 0; - - public ContentWriterIteratingCallback(SeekableByteChannel content, long position, long count, Response target, Callback callback) throws IOException { - this.source = content; - this.sink = target; - this.callback = callback; - this.length = count; - this.source.position(position); - - ByteBufferPool bufferPool = target.getRequest().getComponents().getByteBufferPool(); - int outputBufferSize = target.getRequest().getConnectionMetaData().getHttpConfiguration().getOutputBufferSize(); - boolean useOutputDirectByteBuffers = target.getRequest().getConnectionMetaData().getHttpConfiguration().isUseOutputDirectByteBuffers(); - this.buffer = bufferPool.acquire(outputBufferSize, useOutputDirectByteBuffers); - } - - @Override - protected Action process() throws Throwable { - if (!this.source.isOpen() || this.totalRead == this.length) { - return Action.SUCCEEDED; - } - - ByteBuffer byteBuffer = this.buffer.getByteBuffer(); - BufferUtil.clearToFill(byteBuffer); - byteBuffer.limit((int) Math.min(this.buffer.capacity(), this.length - this.totalRead)); - int read = this.source.read(byteBuffer); - if (read == -1) { - IO.close(this.source); - this.sink.write(true, BufferUtil.EMPTY_BUFFER, this); - return Action.SCHEDULED; - } - this.totalRead += read; - BufferUtil.flipToFlush(byteBuffer, 0); - this.sink.write(false, byteBuffer, this); - return Action.SCHEDULED; - } - - @Override - protected void onCompleteSuccess() { - this.buffer.release(); - IO.close(this.source); - this.callback.succeeded(); - } - - @Override - protected void onCompleteFailure(Throwable x) { - this.buffer.release(); - IO.close(this.source); - this.callback.failed(x); - } - } } From 6380d3a7c2d0524114b49d39b26f7159d1115a47 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 1 Jul 2024 15:38:52 +1000 Subject: [PATCH 145/146] fix checkstyle errors Signed-off-by: Lachlan Roberts --- .../server/reactive/JettyCoreServerHttpResponse.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 74df330a2a45..7427e62828a0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -16,12 +16,7 @@ package org.springframework.http.server.reactive; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.Collections; import java.util.List; import java.util.ListIterator; @@ -29,15 +24,10 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.io.RetainableByteBuffer; -import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IteratingCallback; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; From ca25b6cd01fb0d1f7c65f98b656aa6e51814569b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Mon, 1 Jul 2024 15:48:01 +1000 Subject: [PATCH 146/146] fix failure for CookieIntegrationTests.partitionedAttributeTest Signed-off-by: Lachlan Roberts --- .../http/server/reactive/JettyCoreServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java index 7427e62828a0..3ead620ed00b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyCoreServerHttpResponse.java @@ -69,6 +69,7 @@ public JettyCoreServerHttpResponse(Response response, JettyDataBufferFactory dat .maxAge(httpCookie.getMaxAge()) .sameSite(httpCookie.getSameSite().name()) .secure(httpCookie.isSecure()) + .partitioned(httpCookie.isPartitioned()) .build(); this.addCookie(responseCookie); i.remove(); @@ -224,7 +225,7 @@ public boolean isHttpOnly() { @Override public boolean isPartitioned() { - return false; + return this.responseCookie.isPartitioned(); } @Override