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:
gregw
2024-01-15 19:05:09 +11:00
committed by Simon Baslé
parent b7ec028149
commit 0a60c622cc
33 changed files with 1701 additions and 526 deletions

View File

@@ -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();
}
}
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);