Implement Eclipse Jetty core HTTP handler adapter
This provides an implementation of an HTTP Handler Adapter that is coded directly to the Eclipse Jetty core API, bypassing any servlet implementation. This includes a Jetty implementation of the spring `WebSocketClient` interface, `JettyWebSocketClient`, using an explicit dependency to the jetty-websocket-api. Closes gh-32097 Co-authored-by: Lachlan Roberts <lachlan@webtide.com> Co-authored-by: Arjen Poutsma <arjen.poutsma@broadcom.com>
This commit is contained in:
@@ -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 class JettyClientHttpConnector implements ClientHttpConnector {
|
||||
/**
|
||||
* Set the buffer factory to use.
|
||||
*/
|
||||
public void setBufferFactory(DataBufferFactory bufferFactory) {
|
||||
public void setBufferFactory(JettyDataBufferFactory bufferFactory) {
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
@@ -134,289 +126,9 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
||||
private Mono<ClientHttpResponse> execute(JettyClientHttpRequest request) {
|
||||
return Mono.fromDirect(request.toReactiveRequest()
|
||||
.response((reactiveResponse, chunkPublisher) -> {
|
||||
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this::toDataBuffer);
|
||||
Flux<DataBuffer> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
||||
return Flux.create(sink -> {
|
||||
MultipartParser parser = new MultipartParser(sink, boundary, maxHeadersSize, headersCharset);
|
||||
sink.onCancel(parser::onSinkCancel);
|
||||
sink.onRequest(n -> parser.requestBuffer());
|
||||
buffers.subscribe(parser);
|
||||
});
|
||||
}
|
||||
@@ -112,7 +111,9 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
||||
|
||||
@Override
|
||||
protected void hookOnSubscribe(Subscription subscription) {
|
||||
requestBuffer();
|
||||
if (this.sink.requestedFromDownstream() > 0) {
|
||||
requestBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.http.HttpHeaders;
|
||||
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 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.http.server.reactive;
|
||||
|
||||
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.springframework.core.io.buffer.JettyDataBufferFactory;
|
||||
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 JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory();
|
||||
|
||||
|
||||
public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) {
|
||||
this.httpHandler = httpHandler;
|
||||
}
|
||||
|
||||
public void setDataBufferFactory(JettyDataBufferFactory 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(request, this.dataBufferFactory),
|
||||
new JettyCoreServerHttpResponse(response, this.dataBufferFactory))
|
||||
.subscribe(unused -> {}, callback::failed, callback::succeeded);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.http.server.reactive;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.reactivestreams.FlowAdapters;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.JettyDataBufferFactory;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 final JettyDataBufferFactory dataBufferFactory;
|
||||
|
||||
private final Request request;
|
||||
|
||||
|
||||
public JettyCoreServerHttpRequest(Request request, JettyDataBufferFactory dataBufferFactory) {
|
||||
super(HttpMethod.valueOf(request.getMethod()),
|
||||
request.getHttpURI().toURI(),
|
||||
request.getContext().getContextPath(),
|
||||
new HttpHeaders(new JettyHeadersAdapter(request.getHeaders())));
|
||||
this.dataBufferFactory = dataBufferFactory;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MultiValueMap<String, HttpCookie> initCookies() {
|
||||
List<org.eclipse.jetty.http.HttpCookie> httpCookies = Request.getCookies(this.request);
|
||||
if (httpCookies.isEmpty()) {
|
||||
return CollectionUtils.toMultiValueMap(Collections.emptyMap());
|
||||
}
|
||||
MultiValueMap<String, HttpCookie> cookies =new LinkedMultiValueMap<>();
|
||||
for (org.eclipse.jetty.http.HttpCookie c : httpCookies) {
|
||||
cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue()));
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getNativeRequest() {
|
||||
return (T) this.request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String initId() {
|
||||
return this.request.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public InetSocketAddress getLocalAddress() {
|
||||
SocketAddress localAddress = this.request.getConnectionMetaData().getLocalSocketAddress();
|
||||
return localAddress instanceof InetSocketAddress inet ? inet : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
SocketAddress remoteAddress = this.request.getConnectionMetaData().getRemoteSocketAddress();
|
||||
return remoteAddress instanceof InetSocketAddress inet ? inet : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> 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.dataBufferFactory::wrap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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.http.server.reactive;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.server.HttpCookieUtils;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
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.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.JettyDataBufferFactory;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}.
|
||||
*
|
||||
* @author Greg Wilkins
|
||||
* @author Lachlan Roberts
|
||||
* @since 6.2
|
||||
*/
|
||||
class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage {
|
||||
|
||||
private final Response response;
|
||||
|
||||
public JettyCoreServerHttpResponse(Response response, JettyDataBufferFactory 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
|
||||
for (ListIterator<HttpField> 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())
|
||||
.partitioned(httpCookie.isPartitioned())
|
||||
.build();
|
||||
this.addCookie(responseCookie);
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> writeWithInternal(Publisher<? extends DataBuffer> body) {
|
||||
return Flux.from(body)
|
||||
.concatMap(this::sendDataBuffer)
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<Void> writeAndFlushWithInternal(Publisher<? extends Publisher<? extends DataBuffer>> body) {
|
||||
return Flux.from(body).concatMap(this::writeWithInternal).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyStatusCode() {
|
||||
HttpStatusCode status = getStatusCode();
|
||||
this.response.setStatus(status == null ? 0 : status.value());
|
||||
}
|
||||
|
||||
@Override
|
||||
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<Void> writeWith(Path file, long position, long count) {
|
||||
Callback.Completable callback = new Callback.Completable();
|
||||
Mono<Void> mono = Mono.fromFuture(callback);
|
||||
try {
|
||||
Content.copy(Content.Source.from(null, file, position, count), this.response, callback);
|
||||
}
|
||||
catch (Throwable th) {
|
||||
callback.failed(th);
|
||||
}
|
||||
return doCommit(() -> mono);
|
||||
}
|
||||
|
||||
private Mono<Void> sendDataBuffer(DataBuffer dataBuffer) {
|
||||
return Mono.defer(() -> {
|
||||
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();
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
callback.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCompleteFailure(Throwable cause) {
|
||||
byteBufferIterator.close();
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
callback.failed(cause);
|
||||
}
|
||||
}.iterate();
|
||||
|
||||
return Mono.fromFuture(callback);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getNativeResponse() {
|
||||
return (T) this.response;
|
||||
}
|
||||
|
||||
private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie {
|
||||
private final ResponseCookie responseCookie;
|
||||
|
||||
public ResponseHttpCookie(ResponseCookie responseCookie) {
|
||||
this.responseCookie = responseCookie;
|
||||
}
|
||||
|
||||
public ResponseCookie getResponseCookie() {
|
||||
return this.responseCookie;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.responseCookie.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.responseCookie.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaxAge() {
|
||||
return this.responseCookie.getMaxAge().toSeconds();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getComment() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getDomain() {
|
||||
return this.responseCookie.getDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getPath() {
|
||||
return this.responseCookie.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return this.responseCookie.isSecure();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SameSite getSameSite() {
|
||||
// Adding non-null return site breaks tests.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHttpOnly() {
|
||||
return this.responseCookie.isHttpOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPartitioned() {
|
||||
return this.responseCookie.isPartitioned();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAttributes() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,11 @@
|
||||
package org.springframework.http.support;
|
||||
|
||||
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;
|
||||
|
||||
@@ -44,6 +46,9 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
private final HttpFields headers;
|
||||
|
||||
@Nullable
|
||||
private final HttpFields.Mutable mutable;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@code JettyHeadersAdapter} based on the given
|
||||
@@ -53,6 +58,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
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 +125,36 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@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<String> get(Object key) {
|
||||
if (containsKey(key)) {
|
||||
return this.headers.getValuesList((String) key);
|
||||
List<String> 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 +162,21 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
public List<String> put(String key, List<String> value) {
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
List<String> oldValues = get(key);
|
||||
mutableHttpFields.put(key, value);
|
||||
|
||||
if (oldValues == null) {
|
||||
switch (value.size()) {
|
||||
case 0 -> {}
|
||||
case 1 -> mutableHttpFields.add(key, value.get(0));
|
||||
default -> mutableHttpFields.add(key, value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (value.size()) {
|
||||
case 0 -> mutableHttpFields.remove(key);
|
||||
case 1 -> mutableHttpFields.put(key, value.get(0));
|
||||
default -> mutableHttpFields.put(key, value);
|
||||
}
|
||||
}
|
||||
return oldValues;
|
||||
}
|
||||
|
||||
@@ -150,12 +184,20 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
@Override
|
||||
public List<String> remove(Object key) {
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
List<String> list = null;
|
||||
if (key instanceof String name) {
|
||||
List<String> oldValues = get(key);
|
||||
mutableHttpFields.remove(name);
|
||||
return oldValues;
|
||||
for (ListIterator<HttpField> 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
|
||||
@@ -187,6 +229,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
public Iterator<Entry<String, List<String>>> iterator() {
|
||||
return new EntryIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return headers.size();
|
||||
@@ -195,16 +238,12 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
}
|
||||
|
||||
private HttpFields.Mutable mutableFields() {
|
||||
if (this.headers instanceof HttpFields.Mutable mutableHttpFields) {
|
||||
return mutableHttpFields;
|
||||
}
|
||||
else {
|
||||
if (this.mutable == null) {
|
||||
throw new IllegalStateException("Immutable headers");
|
||||
}
|
||||
return this.mutable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return HttpHeaders.formatHeaders(this);
|
||||
|
||||
Reference in New Issue
Block a user