mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-23 14:40:00 +00:00
Remove Undertow-specific support and testing
Undertow does not support Servlet 6.1, we need to remove compatibility tests as well as Undertow-specific classes for WebSocket and reactive support. Closes gh-35354
This commit is contained in:
+2
-3
@@ -37,9 +37,8 @@ import org.springframework.util.Assert;
|
||||
* event-listener read APIs and Reactive Streams.
|
||||
*
|
||||
* <p>Specifically a base class for reading from the HTTP request body with
|
||||
* Servlet non-blocking I/O and Undertow XNIO as well as handling incoming
|
||||
* WebSocket messages with standard Jakarta WebSocket (JSR-356), Jetty, and
|
||||
* Undertow.
|
||||
* Servlet non-blocking I/O as well as handling incoming
|
||||
* WebSocket messages with standard Jakarta WebSocket (JSR-356), and Jetty.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Violeta Georgieva
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ import org.springframework.util.StringUtils;
|
||||
* event-listener write APIs and Reactive Streams.
|
||||
*
|
||||
* <p>Specifically a base class for writing to the HTTP response body with
|
||||
* Servlet non-blocking I/O and Undertow XNIO as well for writing WebSocket
|
||||
* messages through the Jakarta WebSocket API (JSR-356), Jetty, and Undertow.
|
||||
* Servlet non-blocking I/O as well for writing WebSocket
|
||||
* messages through the Jakarta WebSocket API (JSR-356), and Jetty.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Violeta Georgieva
|
||||
|
||||
-272
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.util.AbstractSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.undertow.util.HeaderMap;
|
||||
import io.undertow.util.HeaderValues;
|
||||
import io.undertow.util.HttpString;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* {@code MultiValueMap} implementation for wrapping Undertow HTTP headers.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
* @since 5.1.1
|
||||
*/
|
||||
class UndertowHeadersAdapter implements MultiValueMap<String, String> {
|
||||
|
||||
private final HeaderMap headers;
|
||||
|
||||
|
||||
UndertowHeadersAdapter(HeaderMap headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getFirst(String key) {
|
||||
return this.headers.getFirst(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(String key, @Nullable String value) {
|
||||
this.headers.add(HttpString.tryFromString(key), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void addAll(String key, List<? extends String> values) {
|
||||
this.headers.addAll(HttpString.tryFromString(key), (List<String>) values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAll(MultiValueMap<String, String> values) {
|
||||
values.forEach((key, list) -> this.headers.addAll(HttpString.tryFromString(key), list));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(String key, @Nullable String value) {
|
||||
this.headers.put(HttpString.tryFromString(key), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAll(Map<String, String> values) {
|
||||
values.forEach((key, list) -> this.headers.put(HttpString.tryFromString(key), list));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> toSingleValueMap() {
|
||||
Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
|
||||
this.headers.forEach(values ->
|
||||
singleValueMap.put(values.getHeaderName().toString(), values.getFirst()));
|
||||
return singleValueMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.headers.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return (this.headers.size() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return (key instanceof String headerName && this.headers.contains(headerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return (value instanceof String &&
|
||||
this.headers.getHeaderNames().stream()
|
||||
.map(this.headers::get)
|
||||
.anyMatch(values -> values.contains(value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> get(Object key) {
|
||||
return (key instanceof String headerName ? this.headers.get(headerName) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> put(String key, List<String> value) {
|
||||
HeaderValues previousValues = this.headers.get(key);
|
||||
this.headers.putAll(HttpString.tryFromString(key), value);
|
||||
return previousValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> remove(Object key) {
|
||||
if (key instanceof String headerName) {
|
||||
Collection<String> removed = this.headers.remove(headerName);
|
||||
if (removed != null) {
|
||||
return new ArrayList<>(removed);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ? extends List<String>> map) {
|
||||
map.forEach((key, values) ->
|
||||
this.headers.putAll(HttpString.tryFromString(key), values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
this.headers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return new HeaderNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<List<String>> values() {
|
||||
return this.headers.getHeaderNames().stream()
|
||||
.map(this.headers::get)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, List<String>>> entrySet() {
|
||||
return new AbstractSet<>() {
|
||||
@Override
|
||||
public Iterator<Entry<String, List<String>>> iterator() {
|
||||
return new EntryIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return headers.size();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return org.springframework.http.HttpHeaders.formatHeaders(this);
|
||||
}
|
||||
|
||||
|
||||
private class EntryIterator implements Iterator<Entry<String, List<String>>> {
|
||||
|
||||
private final Iterator<HttpString> names = headers.getHeaderNames().iterator();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return this.names.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<String, List<String>> next() {
|
||||
return new HeaderEntry(this.names.next());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class HeaderEntry implements Entry<String, List<String>> {
|
||||
|
||||
private final HttpString key;
|
||||
|
||||
HeaderEntry(HttpString key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.key.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getValue() {
|
||||
return headers.get(this.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> setValue(List<String> value) {
|
||||
List<String> previousValues = headers.get(this.key);
|
||||
headers.putAll(this.key, value);
|
||||
return previousValues;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class HeaderNames extends AbstractSet<String> {
|
||||
|
||||
@Override
|
||||
public Iterator<String> iterator() {
|
||||
return new HeaderNamesIterator(headers.getHeaderNames().iterator());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return headers.getHeaderNames().size();
|
||||
}
|
||||
}
|
||||
|
||||
private final class HeaderNamesIterator implements Iterator<String> {
|
||||
|
||||
private final Iterator<HttpString> iterator;
|
||||
|
||||
private @Nullable String currentName;
|
||||
|
||||
private HeaderNamesIterator(Iterator<HttpString> iterator) {
|
||||
this.iterator = iterator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return this.iterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next() {
|
||||
this.currentName = this.iterator.next().toString();
|
||||
return this.currentName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (this.currentName == null) {
|
||||
throw new IllegalStateException("No current Header in iterator");
|
||||
}
|
||||
if (!headers.contains(this.currentName)) {
|
||||
throw new IllegalStateException("Header not present: " + this.currentName);
|
||||
}
|
||||
headers.remove(this.currentName);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import org.apache.commons.logging.Log;
|
||||
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.http.HttpLogging;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Adapt {@link HttpHandler} to the Undertow {@link io.undertow.server.HttpHandler}.
|
||||
*
|
||||
* @author Marek Hawrylczak
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Arjen Poutsma
|
||||
* @since 5.0
|
||||
*/
|
||||
public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler {
|
||||
|
||||
private static final Log logger = HttpLogging.forLogName(UndertowHttpHandlerAdapter.class);
|
||||
|
||||
|
||||
private final HttpHandler httpHandler;
|
||||
|
||||
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||
|
||||
|
||||
public UndertowHttpHandlerAdapter(HttpHandler httpHandler) {
|
||||
Assert.notNull(httpHandler, "HttpHandler must not be null");
|
||||
this.httpHandler = httpHandler;
|
||||
}
|
||||
|
||||
|
||||
public void setDataBufferFactory(DataBufferFactory bufferFactory) {
|
||||
Assert.notNull(bufferFactory, "DataBufferFactory must not be null");
|
||||
this.bufferFactory = bufferFactory;
|
||||
}
|
||||
|
||||
public DataBufferFactory getDataBufferFactory() {
|
||||
return this.bufferFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleRequest(HttpServerExchange exchange) {
|
||||
exchange.dispatch(() -> {
|
||||
UndertowServerHttpRequest request = null;
|
||||
try {
|
||||
request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.debug("Failed to get request URI: " + ex.getMessage());
|
||||
}
|
||||
exchange.setStatusCode(400);
|
||||
return;
|
||||
}
|
||||
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request);
|
||||
|
||||
if (request.getMethod() == HttpMethod.HEAD) {
|
||||
response = new HttpHeadResponseDecorator(response);
|
||||
}
|
||||
|
||||
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request);
|
||||
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static class HandlerResultSubscriber implements Subscriber<Void> {
|
||||
|
||||
private final HttpServerExchange exchange;
|
||||
|
||||
private final String logPrefix;
|
||||
|
||||
|
||||
public HandlerResultSubscriber(HttpServerExchange exchange, UndertowServerHttpRequest request) {
|
||||
this.exchange = exchange;
|
||||
this.logPrefix = request.getLogPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription subscription) {
|
||||
subscription.request(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Void aVoid) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable ex) {
|
||||
logger.trace(this.logPrefix + "Failed to complete: " + ex.getMessage());
|
||||
if (this.exchange.isResponseStarted()) {
|
||||
try {
|
||||
logger.debug(this.logPrefix + "Closing connection");
|
||||
this.exchange.getConnection().close();
|
||||
}
|
||||
catch (IOException ex2) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.debug(this.logPrefix + "Setting HttpServerExchange status to 500 Server Error");
|
||||
this.exchange.setStatusCode(500);
|
||||
this.exchange.endExchange();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
logger.trace(this.logPrefix + "Handling completed");
|
||||
this.exchange.endExchange();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
import io.undertow.connector.ByteBufferPool;
|
||||
import io.undertow.connector.PooledByteBuffer;
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.server.handlers.Cookie;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.xnio.channels.StreamSourceChannel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
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.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Adapt {@link ServerHttpRequest} to the Undertow {@link HttpServerExchange}.
|
||||
*
|
||||
* @author Marek Hawrylczak
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
class UndertowServerHttpRequest extends AbstractServerHttpRequest {
|
||||
|
||||
private static final AtomicLong logPrefixIndex = new AtomicLong();
|
||||
|
||||
|
||||
private final HttpServerExchange exchange;
|
||||
|
||||
private final RequestBodyPublisher body;
|
||||
|
||||
|
||||
public UndertowServerHttpRequest(HttpServerExchange exchange, DataBufferFactory bufferFactory)
|
||||
throws URISyntaxException {
|
||||
|
||||
super(HttpMethod.valueOf(exchange.getRequestMethod().toString()), initUri(exchange), "",
|
||||
new HttpHeaders(new UndertowHeadersAdapter(exchange.getRequestHeaders())));
|
||||
this.exchange = exchange;
|
||||
this.body = new RequestBodyPublisher(exchange, bufferFactory);
|
||||
this.body.registerListeners(exchange);
|
||||
}
|
||||
|
||||
private static URI initUri(HttpServerExchange exchange) throws URISyntaxException {
|
||||
Assert.notNull(exchange, "HttpServerExchange is required");
|
||||
String requestURL = exchange.getRequestURL();
|
||||
String query = exchange.getQueryString();
|
||||
String requestUriAndQuery = (StringUtils.hasLength(query) ? requestURL + "?" + query : requestURL);
|
||||
return new URI(requestUriAndQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MultiValueMap<String, HttpCookie> initCookies() {
|
||||
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
|
||||
for (Cookie cookie : this.exchange.requestCookies()) {
|
||||
HttpCookie httpCookie = new HttpCookie(cookie.getName(), cookie.getValue());
|
||||
cookies.add(cookie.getName(), httpCookie);
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InetSocketAddress getLocalAddress() {
|
||||
return this.exchange.getDestinationAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable InetSocketAddress getRemoteAddress() {
|
||||
return this.exchange.getSourceAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable SslInfo initSslInfo() {
|
||||
SSLSession session = this.exchange.getConnection().getSslSession();
|
||||
if (session != null) {
|
||||
return new DefaultSslInfo(session);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return Flux.from(this.body);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getNativeRequest() {
|
||||
return (T) this.exchange;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String initId() {
|
||||
return ObjectUtils.getIdentityHexString(this.exchange.getConnection()) +
|
||||
"-" + logPrefixIndex.incrementAndGet();
|
||||
}
|
||||
|
||||
|
||||
private class RequestBodyPublisher extends AbstractListenerReadPublisher<DataBuffer> {
|
||||
|
||||
private final StreamSourceChannel channel;
|
||||
|
||||
private final DataBufferFactory bufferFactory;
|
||||
|
||||
private final ByteBufferPool byteBufferPool;
|
||||
|
||||
public RequestBodyPublisher(HttpServerExchange exchange, DataBufferFactory bufferFactory) {
|
||||
super(UndertowServerHttpRequest.this.getLogPrefix());
|
||||
this.channel = exchange.getRequestChannel();
|
||||
this.bufferFactory = bufferFactory;
|
||||
this.byteBufferPool = exchange.getConnection().getByteBufferPool();
|
||||
}
|
||||
|
||||
private void registerListeners(HttpServerExchange exchange) {
|
||||
exchange.addExchangeCompleteListener((ex, next) -> {
|
||||
onAllDataRead();
|
||||
next.proceed();
|
||||
});
|
||||
this.channel.getReadSetter().set(c -> onDataAvailable());
|
||||
this.channel.getCloseSetter().set(c -> onAllDataRead());
|
||||
this.channel.resumeReads();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkOnDataAvailable() {
|
||||
this.channel.resumeReads();
|
||||
// We are allowed to try, it will return null if data is not available
|
||||
onDataAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readingPaused() {
|
||||
this.channel.suspendReads();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable DataBuffer read() throws IOException {
|
||||
PooledByteBuffer pooledByteBuffer = this.byteBufferPool.allocate();
|
||||
try (pooledByteBuffer) {
|
||||
ByteBuffer byteBuffer = pooledByteBuffer.getBuffer();
|
||||
int read = this.channel.read(byteBuffer);
|
||||
|
||||
if (rsReadLogger.isTraceEnabled()) {
|
||||
rsReadLogger.trace(getLogPrefix() + "Read " + read + (read != -1 ? " bytes" : ""));
|
||||
}
|
||||
|
||||
if (read > 0) {
|
||||
byteBuffer.flip();
|
||||
DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(read);
|
||||
dataBuffer.write(byteBuffer);
|
||||
return dataBuffer;
|
||||
}
|
||||
else if (read == -1) {
|
||||
onAllDataRead();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void discardData() {
|
||||
// Nothing to discard since we pass data buffers on immediately..
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-344
@@ -1,344 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.server.handlers.Cookie;
|
||||
import io.undertow.server.handlers.CookieImpl;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.reactivestreams.Processor;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.xnio.channels.StreamSinkChannel;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ZeroCopyHttpOutputMessage;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}.
|
||||
*
|
||||
* @author Marek Hawrylczak
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
class UndertowServerHttpResponse extends AbstractListenerServerHttpResponse implements ZeroCopyHttpOutputMessage {
|
||||
|
||||
private final HttpServerExchange exchange;
|
||||
|
||||
private final UndertowServerHttpRequest request;
|
||||
|
||||
private @Nullable StreamSinkChannel responseChannel;
|
||||
|
||||
|
||||
UndertowServerHttpResponse(
|
||||
HttpServerExchange exchange, DataBufferFactory bufferFactory, UndertowServerHttpRequest request) {
|
||||
|
||||
super(bufferFactory, createHeaders(exchange));
|
||||
this.exchange = exchange;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
private static HttpHeaders createHeaders(HttpServerExchange exchange) {
|
||||
Assert.notNull(exchange, "HttpServerExchange must not be null");
|
||||
UndertowHeadersAdapter headersMap = new UndertowHeadersAdapter(exchange.getResponseHeaders());
|
||||
return new HttpHeaders(headersMap);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T getNativeResponse() {
|
||||
return (T) this.exchange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpStatusCode getStatusCode() {
|
||||
HttpStatusCode status = super.getStatusCode();
|
||||
return (status != null ? status : HttpStatusCode.valueOf(this.exchange.getStatusCode()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyStatusCode() {
|
||||
HttpStatusCode status = super.getStatusCode();
|
||||
if (status != null) {
|
||||
this.exchange.setStatusCode(status.value());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyHeaders() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyCookies() {
|
||||
for (String name : getCookies().keySet()) {
|
||||
for (ResponseCookie httpCookie : getCookies().get(name)) {
|
||||
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
|
||||
if (!httpCookie.getMaxAge().isNegative()) {
|
||||
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
|
||||
}
|
||||
if (httpCookie.getDomain() != null) {
|
||||
cookie.setDomain(httpCookie.getDomain());
|
||||
}
|
||||
if (httpCookie.getPath() != null) {
|
||||
cookie.setPath(httpCookie.getPath());
|
||||
}
|
||||
cookie.setSecure(httpCookie.isSecure());
|
||||
cookie.setHttpOnly(httpCookie.isHttpOnly());
|
||||
// TODO: add "Partitioned" attribute when Undertow supports it
|
||||
cookie.setSameSiteMode(httpCookie.getSameSite());
|
||||
this.exchange.setResponseCookie(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Path file, long position, long count) {
|
||||
return doCommit(() ->
|
||||
Mono.create(sink -> {
|
||||
try {
|
||||
FileChannel source = FileChannel.open(file, StandardOpenOption.READ);
|
||||
TransferBodyListener listener = new TransferBodyListener(source, position, count, sink);
|
||||
sink.onDispose(listener::closeSource);
|
||||
StreamSinkChannel destination = this.exchange.getResponseChannel();
|
||||
destination.getWriteSetter().set(listener::transfer);
|
||||
listener.transfer(destination);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
sink.error(ex);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
|
||||
return new ResponseBodyFlushProcessor();
|
||||
}
|
||||
|
||||
private ResponseBodyProcessor createBodyProcessor() {
|
||||
if (this.responseChannel == null) {
|
||||
this.responseChannel = this.exchange.getResponseChannel();
|
||||
}
|
||||
return new ResponseBodyProcessor(this.responseChannel);
|
||||
}
|
||||
|
||||
|
||||
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
|
||||
|
||||
private final StreamSinkChannel channel;
|
||||
|
||||
private volatile @Nullable ByteBuffer byteBuffer;
|
||||
|
||||
/** Keep track of write listener calls, for {@link #writePossible}. */
|
||||
private volatile boolean writePossible;
|
||||
|
||||
|
||||
public ResponseBodyProcessor(StreamSinkChannel channel) {
|
||||
super(request.getLogPrefix());
|
||||
Assert.notNull(channel, "StreamSinkChannel must not be null");
|
||||
this.channel = channel;
|
||||
this.channel.getWriteSetter().set(c -> {
|
||||
this.writePossible = true;
|
||||
onWritePossible();
|
||||
});
|
||||
this.channel.suspendWrites();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isWritePossible() {
|
||||
this.channel.resumeWrites();
|
||||
return this.writePossible;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean write(DataBuffer dataBuffer) throws IOException {
|
||||
ByteBuffer buffer = this.byteBuffer;
|
||||
if (buffer == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Track write listener calls from here on.
|
||||
this.writePossible = false;
|
||||
|
||||
// In case of IOException, onError handling should call discardData(DataBuffer)..
|
||||
int total = buffer.remaining();
|
||||
int written = writeByteBuffer(buffer);
|
||||
|
||||
if (rsWriteLogger.isTraceEnabled()) {
|
||||
rsWriteLogger.trace(getLogPrefix() + "Wrote " + written + " of " + total + " bytes");
|
||||
}
|
||||
if (written != total) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We wrote all, so can still write more.
|
||||
this.writePossible = true;
|
||||
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
this.byteBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException {
|
||||
int written;
|
||||
int totalWritten = 0;
|
||||
do {
|
||||
written = this.channel.write(byteBuffer);
|
||||
totalWritten += written;
|
||||
}
|
||||
while (byteBuffer.hasRemaining() && written > 0);
|
||||
return totalWritten;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataReceived(DataBuffer dataBuffer) {
|
||||
super.dataReceived(dataBuffer);
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount());
|
||||
dataBuffer.toByteBuffer(byteBuffer);
|
||||
this.byteBuffer = byteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDataEmpty(DataBuffer dataBuffer) {
|
||||
return (dataBuffer.readableByteCount() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writingComplete() {
|
||||
this.channel.getWriteSetter().set(null);
|
||||
this.channel.resumeWrites();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writingFailed(Throwable ex) {
|
||||
cancel();
|
||||
onError(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void discardData(DataBuffer dataBuffer) {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ResponseBodyFlushProcessor extends AbstractListenerWriteFlushProcessor<DataBuffer> {
|
||||
|
||||
public ResponseBodyFlushProcessor() {
|
||||
super(request.getLogPrefix());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Processor<? super DataBuffer, Void> createWriteProcessor() {
|
||||
return UndertowServerHttpResponse.this.createBodyProcessor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void flush() throws IOException {
|
||||
StreamSinkChannel channel = UndertowServerHttpResponse.this.responseChannel;
|
||||
if (channel != null) {
|
||||
if (rsWriteFlushLogger.isTraceEnabled()) {
|
||||
rsWriteFlushLogger.trace(getLogPrefix() + "flush");
|
||||
}
|
||||
channel.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isWritePossible() {
|
||||
StreamSinkChannel channel = UndertowServerHttpResponse.this.responseChannel;
|
||||
if (channel != null) {
|
||||
// We can always call flush, just ensure writes are on.
|
||||
channel.resumeWrites();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isFlushPending() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TransferBodyListener {
|
||||
|
||||
private final FileChannel source;
|
||||
|
||||
private final MonoSink<Void> sink;
|
||||
|
||||
private long position;
|
||||
|
||||
private long count;
|
||||
|
||||
|
||||
public TransferBodyListener(FileChannel source, long position, long count, MonoSink<Void> sink) {
|
||||
this.source = source;
|
||||
this.sink = sink;
|
||||
this.position = position;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public void transfer(StreamSinkChannel destination) {
|
||||
try {
|
||||
while (this.count > 0) {
|
||||
long len = destination.transferFrom(this.source, this.position, this.count);
|
||||
if (len != 0) {
|
||||
this.position += len;
|
||||
this.count -= len;
|
||||
}
|
||||
else {
|
||||
destination.resumeWrites();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.sink.success();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
this.sink.error(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void closeSource() {
|
||||
try {
|
||||
this.source.close();
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* {@link org.springframework.http.server.reactive.HttpHandler} for processing.
|
||||
*
|
||||
* <p>Also provides implementations adapting to different runtimes
|
||||
* including Servlet containers, Netty + Reactor IO, and Undertow.
|
||||
* including Servlet containers and Netty + Reactor IO.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.http.server.reactive;
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
||||
if (dest.isAbsolute() && !dest.exists()) {
|
||||
// Servlet Part.write is not guaranteed to support absolute file paths:
|
||||
// may translate the given path to a relative location within a temp dir
|
||||
// (for example, on Jetty whereas Tomcat and Undertow detect absolute paths).
|
||||
// (for example, on Jetty whereas Tomcat detects absolute paths).
|
||||
// At least we offloaded the file from memory storage; it'll get deleted
|
||||
// from the temp dir eventually in any case. And for our user's purposes,
|
||||
// we can manually copy it to the requested location as a fallback.
|
||||
|
||||
Reference in New Issue
Block a user