Upgrade to Jetty 12
This commit upgrades Spring Framework to Jetty 12.0.1, and Reactive HTTP Client 4.0.0. Closes gh-30698
This commit is contained in:
@@ -15,7 +15,8 @@ dependencies {
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.14"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.20.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:11.0.16"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.1"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.1"))
|
||||
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.0"))
|
||||
@@ -115,7 +116,7 @@ dependencies {
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.3.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.0")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
|
||||
@@ -48,10 +48,7 @@ dependencies {
|
||||
optional("org.eclipse.jetty:jetty-server") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
optional("org.eclipse.jetty:jetty-servlet") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
optional("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.0.beta2") {
|
||||
optional("org.eclipse.jetty.ee10:jetty-ee10-servlet") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
exclude group: "org.eclipse.jetty", module: "jetty-ee"
|
||||
exclude group: "org.eclipse.jetty", module: "jetty-security"
|
||||
@@ -73,7 +70,7 @@ dependencies {
|
||||
testFixturesImplementation("org.bouncycastle:bcpkix-jdk18on") {
|
||||
because("needed by Netty's SelfSignedCertificate on JDK 15+")
|
||||
}
|
||||
testFixturesImplementation("org.eclipse.jetty.websocket:websocket-jetty-server")
|
||||
testFixturesImplementation("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server")
|
||||
testImplementation(project(":spring-core-test"))
|
||||
testImplementation(testFixtures(project(":spring-beans")))
|
||||
testImplementation(testFixtures(project(":spring-context")))
|
||||
@@ -89,7 +86,7 @@ dependencies {
|
||||
testImplementation("org.apache.tomcat:tomcat-util")
|
||||
testImplementation("org.apache.tomcat.embed:tomcat-embed-core")
|
||||
testImplementation("org.eclipse.jetty:jetty-server")
|
||||
testImplementation("org.eclipse.jetty:jetty-servlet")
|
||||
testImplementation("org.eclipse.jetty.ee10:jetty-ee10-servlet")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
testImplementation("org.skyscreamer:jsonassert")
|
||||
testImplementation("org.xmlunit:xmlunit-assertj")
|
||||
|
||||
@@ -24,10 +24,10 @@ import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.util.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
import org.eclipse.jetty.client.Response;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.springframework.http.client;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.Response;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,18 +17,24 @@
|
||||
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.api.Request;
|
||||
import org.eclipse.jetty.reactive.client.ContentChunk;
|
||||
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.http.HttpMethod;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -133,19 +139,284 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
|
||||
}));
|
||||
}
|
||||
|
||||
private DataBuffer toDataBuffer(ContentChunk chunk) {
|
||||
private DataBuffer toDataBuffer(Content.Chunk chunk) {
|
||||
DataBuffer delegate = this.bufferFactory.wrap(chunk.getByteBuffer());
|
||||
return new JettyDataBuffer(delegate, chunk);
|
||||
}
|
||||
|
||||
// Originally we copy due to do:
|
||||
// https://github.com/eclipse/jetty.project/issues/2429
|
||||
|
||||
// Now that the issue is marked fixed we need to replace the below with a
|
||||
// PooledDataBuffer that adapts "release()" to "succeeded()", and also
|
||||
// evaluate if the concern here is addressed.
|
||||
private static final class JettyDataBuffer implements PooledDataBuffer {
|
||||
|
||||
DataBuffer buffer = this.bufferFactory.allocateBuffer(chunk.buffer.capacity());
|
||||
buffer.write(chunk.buffer);
|
||||
chunk.callback.succeeded();
|
||||
return buffer;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
|
||||
package org.springframework.http.client.reactive;
|
||||
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.reactive.client.ContentChunk;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.reactive.client.ReactiveRequest;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
@@ -92,7 +92,9 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest {
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
return Mono.<Void>create(sink -> {
|
||||
ReactiveRequest.Content content = Flux.from(body)
|
||||
.map(buffer -> toContentChunk(buffer, sink))
|
||||
.concatMapIterable(this::toContentChunks)
|
||||
.concatWith(Mono.just(Content.Chunk.EOF))
|
||||
.doOnError(sink::error)
|
||||
.as(chunks -> ReactiveRequest.Content.fromPublisher(chunks, getContentType()));
|
||||
this.builder.content(content);
|
||||
sink.success();
|
||||
@@ -111,26 +113,28 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest {
|
||||
return contentType != null ? contentType.toString() : MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
|
||||
private ContentChunk toContentChunk(DataBuffer dataBuffer, MonoSink<Void> sink) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount());
|
||||
dataBuffer.toByteBuffer(byteBuffer);
|
||||
return new ContentChunk(byteBuffer, new Callback() {
|
||||
@Override
|
||||
public void succeeded() {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
@Override
|
||||
public void failed(Throwable t) {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
sink.error(t);
|
||||
}
|
||||
});
|
||||
private List<Content.Chunk> toContentChunks(DataBuffer dataBuffer) {
|
||||
|
||||
List<Content.Chunk> result = new ArrayList<>(1);
|
||||
DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers();
|
||||
while (iterator.hasNext()) {
|
||||
ByteBuffer byteBuffer = iterator.next();
|
||||
boolean last = !iterator.hasNext();
|
||||
Content.Chunk chunk = Content.Chunk.from(byteBuffer, false, () -> {
|
||||
if (last) {
|
||||
iterator.close();
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
});
|
||||
result.add(chunk);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyCookies() {
|
||||
getCookies().values().stream().flatMap(Collection::stream)
|
||||
.map(cookie -> new HttpCookie(cookie.getName(), cookie.getValue()))
|
||||
.map(cookie -> HttpCookie.build(cookie.getName(), cookie.getValue()).build())
|
||||
.forEach(this.jettyRequest::cookie);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ package org.springframework.http.client.reactive;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.eclipse.jetty.io.ArrayByteBufferPool;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.MappedByteBufferPool;
|
||||
import org.eclipse.jetty.util.ProcessorUtils;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
@@ -69,7 +69,7 @@ public class JettyResourceFactory implements InitializingBean, DisposableBean {
|
||||
|
||||
/**
|
||||
* Configure the {@link ByteBufferPool} to use.
|
||||
* <p>By default, initialized with a {@link MappedByteBufferPool}.
|
||||
* <p>By default, initialized with a {@link ArrayByteBufferPool}.
|
||||
* @param byteBufferPool the {@link ByteBuffer} pool to use
|
||||
*/
|
||||
public void setByteBufferPool(@Nullable ByteBufferPool byteBufferPool) {
|
||||
@@ -130,7 +130,7 @@ public class JettyResourceFactory implements InitializingBean, DisposableBean {
|
||||
this.executor = threadPool;
|
||||
}
|
||||
if (this.byteBufferPool == null) {
|
||||
this.byteBufferPool = new MappedByteBufferPool(2048,
|
||||
this.byteBufferPool = new ArrayByteBufferPool(0, 2048, 65536, // from HttpClient:202
|
||||
this.executor instanceof ThreadPool.SizedThreadPool sizedThreadPool ?
|
||||
sizedThreadPool.getMaxThreads() / 2 :
|
||||
ProcessorUtils.availableProcessors() * 2);
|
||||
|
||||
@@ -18,26 +18,14 @@ package org.springframework.http.server.reactive;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletResponseWrapper;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.server.HttpOutput;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.ee10.servlet.HttpOutput;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.support.JettyHeadersAdapter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* {@link ServletHttpHandlerAdapter} extension that uses Jetty APIs for writing
|
||||
@@ -51,128 +39,18 @@ import org.springframework.util.MultiValueMap;
|
||||
*/
|
||||
public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
|
||||
|
||||
private static final boolean jetty11Present = ClassUtils.isPresent(
|
||||
"org.eclipse.jetty.server.HttpOutput", JettyHttpHandlerAdapter.class.getClassLoader());
|
||||
|
||||
private static final boolean jetty12Present = ClassUtils.isPresent(
|
||||
"org.eclipse.jetty.ee10.servlet.HttpOutput", JettyHttpHandlerAdapter.class.getClassLoader());
|
||||
|
||||
|
||||
public JettyHttpHandlerAdapter(HttpHandler httpHandler) {
|
||||
super(httpHandler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ServletServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context)
|
||||
throws IOException, URISyntaxException {
|
||||
|
||||
if (jetty11Present) {
|
||||
Assert.state(getServletPath() != null, "Servlet path is not initialized");
|
||||
return new Jetty11ServerHttpRequest(
|
||||
request, context, getServletPath(), getDataBufferFactory(), getBufferSize());
|
||||
}
|
||||
else {
|
||||
return super.createRequest(request, context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServletServerHttpResponse createResponse(HttpServletResponse response,
|
||||
AsyncContext context, ServletServerHttpRequest request) throws IOException {
|
||||
|
||||
if (jetty11Present) {
|
||||
return new Jetty11ServerHttpResponse(
|
||||
response, context, getDataBufferFactory(), getBufferSize(), request);
|
||||
}
|
||||
else if (jetty12Present) {
|
||||
return new Jetty12ServerHttpResponse(
|
||||
response, context, getDataBufferFactory(), getBufferSize(), request);
|
||||
}
|
||||
else {
|
||||
return super.createResponse(response, context, request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class Jetty11ServerHttpRequest extends ServletServerHttpRequest {
|
||||
|
||||
Jetty11ServerHttpRequest(HttpServletRequest request, AsyncContext asyncContext,
|
||||
String servletPath, DataBufferFactory bufferFactory, int bufferSize)
|
||||
throws IOException, URISyntaxException {
|
||||
|
||||
super(createHeaders(request), request, asyncContext, servletPath, bufferFactory, bufferSize);
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> createHeaders(HttpServletRequest servletRequest) {
|
||||
Request request = getRequest(servletRequest);
|
||||
return new JettyHeadersAdapter(HttpFields.build(request.getHttpFields()));
|
||||
}
|
||||
|
||||
private static Request getRequest(HttpServletRequest request) {
|
||||
if (request instanceof Request jettyRequest) {
|
||||
return jettyRequest;
|
||||
}
|
||||
else if (request instanceof HttpServletRequestWrapper wrapper) {
|
||||
HttpServletRequest wrappedRequest = (HttpServletRequest) wrapper.getRequest();
|
||||
return getRequest(wrappedRequest);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Cannot convert [" + request.getClass() +
|
||||
"] to org.eclipse.jetty.server.Request");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class Jetty11ServerHttpResponse extends ServletServerHttpResponse {
|
||||
|
||||
Jetty11ServerHttpResponse(HttpServletResponse response, AsyncContext asyncContext,
|
||||
DataBufferFactory bufferFactory, int bufferSize, ServletServerHttpRequest request)
|
||||
throws IOException {
|
||||
|
||||
super(createHeaders(response), response, asyncContext, bufferFactory, bufferSize, request);
|
||||
}
|
||||
|
||||
private static HttpHeaders createHeaders(HttpServletResponse servletResponse) {
|
||||
Response response = getResponse(servletResponse);
|
||||
return new HttpHeaders(new JettyHeadersAdapter(response.getHttpFields()));
|
||||
}
|
||||
|
||||
private static Response getResponse(HttpServletResponse response) {
|
||||
if (response instanceof Response jettyResponse) {
|
||||
return jettyResponse;
|
||||
}
|
||||
else if (response instanceof HttpServletResponseWrapper wrapper) {
|
||||
HttpServletResponse wrappedResponse = (HttpServletResponse) wrapper.getResponse();
|
||||
return getResponse(wrappedResponse);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Cannot convert [" + response.getClass() +
|
||||
"] to org.eclipse.jetty.server.Response");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException {
|
||||
if (getOutputStream() instanceof HttpOutput httpOutput) {
|
||||
int len = 0;
|
||||
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
|
||||
while (iterator.hasNext() && httpOutput.isReady()) {
|
||||
ByteBuffer byteBuffer = iterator.next();
|
||||
len += byteBuffer.remaining();
|
||||
httpOutput.write(byteBuffer);
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
return super.writeToOutputStream(dataBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyHeaders() {
|
||||
adaptHeaders(false);
|
||||
}
|
||||
return new Jetty12ServerHttpResponse(
|
||||
response, context, getDataBufferFactory(), getBufferSize(), request);
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +66,7 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
|
||||
@Override
|
||||
protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException {
|
||||
OutputStream output = getOutputStream();
|
||||
if (output instanceof org.eclipse.jetty.ee10.servlet.HttpOutput httpOutput) {
|
||||
if (output instanceof HttpOutput httpOutput) {
|
||||
int len = 0;
|
||||
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
|
||||
while (iterator.hasNext() && httpOutput.isReady()) {
|
||||
|
||||
@@ -44,8 +44,6 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
private final HttpFields headers;
|
||||
|
||||
private static final String IMMUTABLE_HEADER_ERROR = "Immutable headers";
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@code JettyHeadersAdapter} based on the given
|
||||
@@ -65,10 +63,10 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@Override
|
||||
public void add(String key, @Nullable String value) {
|
||||
if (!(this.headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
if (value != null) {
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
mutableHttpFields.add(key, value);
|
||||
}
|
||||
mutableHttpFields.add(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,10 +81,13 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@Override
|
||||
public void set(String key, @Nullable String value) {
|
||||
if (!(this.headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
if (value != null) {
|
||||
mutableHttpFields.put(key, value);
|
||||
}
|
||||
else {
|
||||
mutableHttpFields.remove(key);
|
||||
}
|
||||
mutableHttpFields.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,9 +140,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> put(String key, List<String> value) {
|
||||
if (!(this.headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
}
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
List<String> oldValues = get(key);
|
||||
mutableHttpFields.put(key, value);
|
||||
return oldValues;
|
||||
@@ -150,9 +149,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> remove(Object key) {
|
||||
if (!(this.headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
}
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
if (key instanceof String name) {
|
||||
List<String> oldValues = get(key);
|
||||
mutableHttpFields.remove(name);
|
||||
@@ -168,9 +165,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
if (!(this.headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
}
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
mutableHttpFields.clear();
|
||||
}
|
||||
|
||||
@@ -199,6 +194,16 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
};
|
||||
}
|
||||
|
||||
private HttpFields.Mutable mutableFields() {
|
||||
if (this.headers instanceof HttpFields.Mutable mutableHttpFields) {
|
||||
return mutableHttpFields;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Immutable headers");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@@ -242,9 +247,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@Override
|
||||
public List<String> setValue(List<String> value) {
|
||||
if (!(headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
}
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
List<String> previousValues = headers.getValuesList(this.key);
|
||||
mutableHttpFields.put(this.key, value);
|
||||
return previousValues;
|
||||
@@ -290,9 +293,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (!(headers instanceof HttpFields.Mutable mutableHttpFields)) {
|
||||
throw new IllegalStateException(IMMUTABLE_HEADER_ERROR);
|
||||
}
|
||||
HttpFields.Mutable mutableHttpFields = mutableFields();
|
||||
if (this.currentName == null) {
|
||||
throw new IllegalStateException("No current Header in iterator");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.bind.support;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
@@ -23,11 +25,11 @@ import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.Part;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.NetworkConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -37,6 +39,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@@ -61,6 +64,8 @@ class WebRequestDataBinderIntegrationTests {
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
private Path tempDirectory;
|
||||
|
||||
|
||||
@BeforeAll
|
||||
void startJettyServer() throws Exception {
|
||||
@@ -69,7 +74,9 @@ class WebRequestDataBinderIntegrationTests {
|
||||
|
||||
ServletContextHandler handler = new ServletContextHandler();
|
||||
|
||||
MultipartConfigElement multipartConfig = new MultipartConfigElement("");
|
||||
this.tempDirectory = Files.createTempDirectory("WebRequestDataBinderIntegrationTests");
|
||||
|
||||
MultipartConfigElement multipartConfig = new MultipartConfigElement(this.tempDirectory.toString());
|
||||
|
||||
ServletHolder holder = new ServletHolder(partsServlet);
|
||||
holder.getRegistration().setMultipartConfig(multipartConfig);
|
||||
@@ -89,8 +96,13 @@ class WebRequestDataBinderIntegrationTests {
|
||||
|
||||
@AfterAll
|
||||
void stopJettyServer() throws Exception {
|
||||
if (jettyServer != null) {
|
||||
jettyServer.stop();
|
||||
try {
|
||||
if (jettyServer != null) {
|
||||
jettyServer.stop();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
FileSystemUtils.deleteRecursively(this.tempDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
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.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
|
||||
import org.springframework.http.server.reactive.JettyHttpHandlerAdapter;
|
||||
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
|
||||
@@ -45,15 +45,16 @@ public class JettyHttpServer extends AbstractHttpServer {
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
servletHolder.setAsyncSupported(true);
|
||||
|
||||
this.contextHandler = new ServletContextHandler(this.jettyServer, "", false, false);
|
||||
this.contextHandler = new ServletContextHandler("", false, false);
|
||||
this.contextHandler.addServlet(servletHolder, "/");
|
||||
this.contextHandler.addServletContainerInitializer(new JettyWebSocketServletContainerInitializer());
|
||||
this.contextHandler.start();
|
||||
|
||||
ServerConnector connector = new ServerConnector(this.jettyServer);
|
||||
connector.setHost(getHost());
|
||||
connector.setPort(getPort());
|
||||
this.jettyServer.addConnector(connector);
|
||||
this.jettyServer.setHandler(this.contextHandler);
|
||||
this.contextHandler.start();
|
||||
}
|
||||
|
||||
private ServletHttpHandlerAdapter createServletAdapter() {
|
||||
|
||||
@@ -24,8 +24,7 @@ dependencies {
|
||||
exclude group: "org.apache.tomcat", module: "tomcat-servlet-api"
|
||||
exclude group: "org.apache.tomcat", module: "tomcat-websocket-api"
|
||||
}
|
||||
optional("org.eclipse.jetty.websocket:websocket-jetty-client")
|
||||
optional("org.eclipse.jetty.websocket:websocket-jetty-server") {
|
||||
optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
optional("org.freemarker:freemarker")
|
||||
@@ -50,7 +49,6 @@ dependencies {
|
||||
testImplementation("org.apache.tomcat.embed:tomcat-embed-core")
|
||||
testImplementation("org.eclipse.jetty:jetty-reactive-httpclient")
|
||||
testImplementation("org.eclipse.jetty:jetty-server")
|
||||
testImplementation("org.eclipse.jetty:jetty-servlet")
|
||||
testImplementation("org.hibernate:hibernate-validator")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-script-runtime")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
||||
|
||||
@@ -20,13 +20,14 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.OnWebSocketConnect;
|
||||
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;
|
||||
|
||||
@@ -71,8 +72,8 @@ public class JettyWebSocketHandlerAdapter {
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
@OnWebSocketOpen
|
||||
public void onWebSocketOpen(Session session) {
|
||||
this.delegateSession = this.sessionFactory.apply(session);
|
||||
this.delegateHandler.handle(this.delegateSession)
|
||||
.checkpoint(session.getUpgradeRequest().getRequestURI() + " [JettyWebSocketHandlerAdapter]")
|
||||
@@ -88,21 +89,22 @@ public class JettyWebSocketHandlerAdapter {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketBinary(byte[] message, int offset, int length) {
|
||||
public void onWebSocketBinary(ByteBuffer buffer, Callback callback) {
|
||||
if (this.delegateSession != null) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(message, offset, length);
|
||||
WebSocketMessage webSocketMessage = toMessage(Type.BINARY, buffer);
|
||||
this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage);
|
||||
callback.succeed();
|
||||
}
|
||||
}
|
||||
|
||||
@OnWebSocketFrame
|
||||
public void onWebSocketFrame(Frame frame) {
|
||||
public void onWebSocketFrame(Frame frame, Callback callback) {
|
||||
if (this.delegateSession != null) {
|
||||
if (OpCode.PONG == frame.getOpCode()) {
|
||||
ByteBuffer buffer = (frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD);
|
||||
WebSocketMessage webSocketMessage = toMessage(Type.PONG, buffer);
|
||||
this.delegateSession.handleMessage(webSocketMessage.getType(), webSocketMessage);
|
||||
callback.succeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,14 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.SuspendToken;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
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;
|
||||
@@ -47,10 +44,6 @@ import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
*/
|
||||
public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Session> {
|
||||
|
||||
@Nullable
|
||||
private volatile SuspendToken suspendToken;
|
||||
|
||||
|
||||
public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) {
|
||||
this(session, info, factory, null);
|
||||
}
|
||||
@@ -66,32 +59,26 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
|
||||
|
||||
@Override
|
||||
protected boolean canSuspendReceiving() {
|
||||
return true;
|
||||
// Jetty 12 TODO: research suspend functionality in Jetty 12
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void suspendReceiving() {
|
||||
Assert.state(this.suspendToken == null, "Already suspended");
|
||||
this.suspendToken = getDelegate().suspend();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resumeReceiving() {
|
||||
SuspendToken tokenToUse = this.suspendToken;
|
||||
this.suspendToken = null;
|
||||
if (tokenToUse != null) {
|
||||
tokenToUse.resume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean sendMessage(WebSocketMessage message) throws IOException {
|
||||
DataBuffer dataBuffer = message.getPayload();
|
||||
RemoteEndpoint remote = getDelegate().getRemote();
|
||||
Session session = getDelegate();
|
||||
if (WebSocketMessage.Type.TEXT.equals(message.getType())) {
|
||||
getSendProcessor().setReadyToSend(false);
|
||||
String text = dataBuffer.toString(StandardCharsets.UTF_8);
|
||||
remote.sendString(text, new SendProcessorCallback());
|
||||
session.sendText(text, new SendProcessorCallback());
|
||||
}
|
||||
else {
|
||||
if (WebSocketMessage.Type.BINARY.equals(message.getType())) {
|
||||
@@ -101,9 +88,9 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
|
||||
while (iterator.hasNext()) {
|
||||
ByteBuffer byteBuffer = iterator.next();
|
||||
switch (message.getType()) {
|
||||
case BINARY -> remote.sendBytes(byteBuffer, new SendProcessorCallback());
|
||||
case PING -> remote.sendPing(byteBuffer);
|
||||
case PONG -> remote.sendPong(byteBuffer);
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -119,21 +106,23 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
|
||||
|
||||
@Override
|
||||
public Mono<Void> close(CloseStatus status) {
|
||||
getDelegate().close(status.getCode(), status.getReason());
|
||||
return Mono.empty();
|
||||
Callback.Completable callback = new Callback.Completable();
|
||||
getDelegate().close(status.getCode(), status.getReason(), callback);
|
||||
|
||||
return Mono.fromFuture(callback);
|
||||
}
|
||||
|
||||
|
||||
private final class SendProcessorCallback implements WriteCallback {
|
||||
private final class SendProcessorCallback implements Callback {
|
||||
|
||||
@Override
|
||||
public void writeFailed(Throwable x) {
|
||||
public void fail(Throwable x) {
|
||||
getSendProcessor().cancel();
|
||||
getSendProcessor().onError(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSuccess() {
|
||||
public void succeed() {
|
||||
getSendProcessor().setReadyToSend(true);
|
||||
getSendProcessor().onWritePossible();
|
||||
}
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
import java.net.URI;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* A {@link WebSocketClient} implementation for use with Jetty
|
||||
* {@link org.eclipse.jetty.websocket.client.WebSocketClient}.
|
||||
* Only supported on Jetty 11, superseded by {@link StandardWebSocketClient}.
|
||||
*
|
||||
* <p><strong>Note: </strong> the Jetty {@code WebSocketClient} requires
|
||||
* lifecycle management and must be started and stopped. This is automatically
|
||||
* managed when this class is declared as a Spring bean and created with the
|
||||
* default constructor. See constructor notes for more details.
|
||||
*
|
||||
* @author Violeta Georgieva
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.0.3, in favor of {@link StandardWebSocketClient}
|
||||
*/
|
||||
@Deprecated(since = "6.0.3", forRemoval = true)
|
||||
public class JettyWebSocketClient implements WebSocketClient, Lifecycle {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(JettyWebSocketClient.class);
|
||||
|
||||
private final org.eclipse.jetty.websocket.client.WebSocketClient jettyClient;
|
||||
|
||||
private final boolean externallyManaged;
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor that creates and manages an instance of a Jetty
|
||||
* {@link org.eclipse.jetty.websocket.client.WebSocketClient WebSocketClient}.
|
||||
* The instance can be obtained with {@link #getJettyClient()} for further
|
||||
* configuration.
|
||||
* <p><strong>Note: </strong> When this constructor is used {@link Lifecycle}
|
||||
* methods of this class are delegated to the Jetty {@code WebSocketClient}.
|
||||
*/
|
||||
public JettyWebSocketClient() {
|
||||
this.jettyClient = new org.eclipse.jetty.websocket.client.WebSocketClient();
|
||||
this.externallyManaged = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that accepts an existing instance of a Jetty
|
||||
* {@link org.eclipse.jetty.websocket.client.WebSocketClient WebSocketClient}.
|
||||
* <p><strong>Note: </strong> Use of this constructor implies the Jetty
|
||||
* {@code WebSocketClient} is externally managed and hence {@link Lifecycle}
|
||||
* methods of this class are not delegated to it.
|
||||
*/
|
||||
public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient jettyClient) {
|
||||
this.jettyClient = jettyClient;
|
||||
this.externallyManaged = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the underlying Jetty {@code WebSocketClient}.
|
||||
*/
|
||||
public org.eclipse.jetty.websocket.client.WebSocketClient getJettyClient() {
|
||||
return this.jettyClient;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (!this.externallyManaged) {
|
||||
try {
|
||||
this.jettyClient.start();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to start Jetty WebSocketClient", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (!this.externallyManaged) {
|
||||
try {
|
||||
this.jettyClient.stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Error stopping Jetty WebSocketClient", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.jettyClient.isRunning();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> execute(URI url, WebSocketHandler handler) {
|
||||
return execute(url, new HttpHeaders(), handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> execute(URI url, HttpHeaders headers, WebSocketHandler handler) {
|
||||
return executeInternal(url, headers, handler);
|
||||
}
|
||||
|
||||
private Mono<Void> executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) {
|
||||
Sinks.Empty<Void> completionSink = Sinks.empty();
|
||||
return Mono.deferContextual(contextView -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Connecting to " + url);
|
||||
}
|
||||
Object jettyHandler = createHandler(
|
||||
url, ContextWebSocketHandler.decorate(handler, contextView), completionSink);
|
||||
ClientUpgradeRequest request = new ClientUpgradeRequest();
|
||||
request.setHeaders(headers);
|
||||
request.setSubProtocols(handler.getSubProtocols());
|
||||
try {
|
||||
this.jettyClient.connect(jettyHandler, url, request);
|
||||
return completionSink.asMono();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Object createHandler(URI url, WebSocketHandler handler, Sinks.Empty<Void> completion) {
|
||||
Function<Session, JettyWebSocketSession> sessionFactory = session -> {
|
||||
HandshakeInfo info = createHandshakeInfo(url, session);
|
||||
return new JettyWebSocketSession(session, info, DefaultDataBufferFactory.sharedInstance, completion);
|
||||
};
|
||||
return new JettyWebSocketHandlerAdapter(handler, sessionFactory);
|
||||
}
|
||||
|
||||
private HandshakeInfo createHandshakeInfo(URI url, Session jettySession) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.putAll(jettySession.getUpgradeResponse().getHeaders());
|
||||
String protocol = headers.getFirst("Sec-WebSocket-Protocol");
|
||||
return new HandshakeInfo(url, headers, Mono.empty(), protocol);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import java.util.function.Supplier;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -93,7 +93,6 @@ abstract class AbstractReactiveWebSocketIntegrationTests {
|
||||
@SuppressWarnings("removal")
|
||||
WebSocketClient[] clients = new WebSocketClient[] {
|
||||
new TomcatWebSocketClient(),
|
||||
new org.springframework.web.reactive.socket.client.JettyWebSocketClient(),
|
||||
new ReactorNettyWebSocketClient(),
|
||||
new ReactorNetty2WebSocketClient(),
|
||||
new UndertowWebSocketClient(Xnio.getInstance().createWorker(OptionMap.EMPTY))
|
||||
|
||||
@@ -55,10 +55,7 @@ dependencies {
|
||||
exclude group: "pull-parser", module: "pull-parser"
|
||||
exclude group: "xpp3", module: "xpp3"
|
||||
}
|
||||
testImplementation("org.eclipse.jetty:jetty-server") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
testImplementation("org.eclipse.jetty:jetty-servlet") {
|
||||
testImplementation("org.eclipse.jetty.ee10:jetty-ee10-servlet") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
testImplementation("org.hibernate:hibernate-validator")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -18,6 +18,8 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -25,11 +27,11 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.ee10.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.NetworkConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -51,6 +53,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
@@ -83,18 +86,22 @@ class RequestPartIntegrationTests {
|
||||
|
||||
private static String baseUrl;
|
||||
|
||||
private static Path tempDirectory;
|
||||
|
||||
|
||||
@BeforeAll
|
||||
static void startServer() throws Exception {
|
||||
// Let server pick its own random, available port.
|
||||
server = new Server(0);
|
||||
|
||||
tempDirectory = Files.createTempDirectory("RequestPartIntegrationTests");
|
||||
|
||||
ServletContextHandler handler = new ServletContextHandler();
|
||||
handler.setContextPath("/");
|
||||
ServletHolder standardResolverServlet = new ServletHolder(DispatcherServlet.class);
|
||||
standardResolverServlet.setInitParameter("contextConfigLocation", StandardMultipartResolverTestConfig.class.getName());
|
||||
standardResolverServlet.setInitParameter("contextClass", AnnotationConfigWebApplicationContext.class.getName());
|
||||
standardResolverServlet.getRegistration().setMultipartConfig(new MultipartConfigElement(""));
|
||||
standardResolverServlet.getRegistration().setMultipartConfig(new MultipartConfigElement(tempDirectory.toString()));
|
||||
handler.addServlet(standardResolverServlet, "/standard-resolver/*");
|
||||
|
||||
server.setHandler(handler);
|
||||
@@ -107,8 +114,13 @@ class RequestPartIntegrationTests {
|
||||
|
||||
@AfterAll
|
||||
static void stopServer() throws Exception {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
try {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
FileSystemUtils.deleteRecursively(tempDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +161,7 @@ class RequestPartIntegrationTests {
|
||||
"Content-Length: 7\r\n" +
|
||||
"\r\n" +
|
||||
"content\r\n" +
|
||||
"--" + boundaryText + "--";
|
||||
"--" + boundaryText + "--\r\n ";
|
||||
|
||||
RequestEntity<byte[]> requestEntity =
|
||||
RequestEntity.post(URI.create(baseUrl + "/standard-resolver/spr13319"))
|
||||
|
||||
@@ -16,14 +16,13 @@ dependencies {
|
||||
exclude group: "org.apache.tomcat", module: "tomcat-servlet-api"
|
||||
exclude group: "org.apache.tomcat", module: "tomcat-websocket-api"
|
||||
}
|
||||
optional("org.eclipse.jetty:jetty-webapp") {
|
||||
optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") {
|
||||
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
|
||||
}
|
||||
optional("org.eclipse.jetty.websocket:websocket-jetty-server") {
|
||||
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"
|
||||
}
|
||||
optional("org.eclipse.jetty.websocket:websocket-jetty-client")
|
||||
optional("org.eclipse.jetty:jetty-client")
|
||||
optional("org.glassfish.tyrus:tyrus-container-servlet")
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
testImplementation(testFixtures(project(":spring-web")))
|
||||
|
||||
@@ -20,13 +20,14 @@ import java.nio.ByteBuffer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
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.OnWebSocketConnect;
|
||||
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;
|
||||
|
||||
@@ -65,8 +66,8 @@ public class JettyWebSocketHandlerAdapter {
|
||||
}
|
||||
|
||||
|
||||
@OnWebSocketConnect
|
||||
public void onWebSocketConnect(Session session) {
|
||||
@OnWebSocketOpen
|
||||
public void onWebSocketOpen(Session session) {
|
||||
try {
|
||||
this.wsSession.initializeNativeSession(session);
|
||||
this.webSocketHandler.afterConnectionEstablished(this.wsSession);
|
||||
@@ -88,8 +89,8 @@ public class JettyWebSocketHandlerAdapter {
|
||||
}
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketBinary(byte[] payload, int offset, int length) {
|
||||
BinaryMessage message = new BinaryMessage(payload, offset, length, true);
|
||||
public void onWebSocketBinary(ByteBuffer payload, Callback callback) {
|
||||
BinaryMessage message = new BinaryMessage(payload, true);
|
||||
try {
|
||||
this.webSocketHandler.handleMessage(this.wsSession, message);
|
||||
}
|
||||
@@ -99,7 +100,7 @@ public class JettyWebSocketHandlerAdapter {
|
||||
}
|
||||
|
||||
@OnWebSocketFrame
|
||||
public void onWebSocketFrame(Frame frame) {
|
||||
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(payload);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.web.socket.adapter.jetty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
@@ -24,9 +25,10 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Callback;
|
||||
import org.eclipse.jetty.websocket.api.ExtensionConfig;
|
||||
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -131,18 +133,17 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
|
||||
@Override
|
||||
public InetSocketAddress getLocalAddress() {
|
||||
checkNativeSessionInitialized();
|
||||
return (InetSocketAddress) getNativeSession().getLocalAddress();
|
||||
return (InetSocketAddress) getNativeSession().getLocalSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
checkNativeSessionInitialized();
|
||||
return (InetSocketAddress) getNativeSession().getRemoteAddress();
|
||||
return (InetSocketAddress) getNativeSession().getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is a no-op for Jetty. As per {@link Session#getPolicy()}, the
|
||||
* returned {@code WebSocketPolicy} is read-only and changing it has no effect.
|
||||
* This method is a no-op for Jetty.
|
||||
*/
|
||||
@Override
|
||||
public void setTextMessageSizeLimit(int messageSizeLimit) {
|
||||
@@ -155,8 +156,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is a no-op for Jetty. As per {@link Session#getPolicy()}, the
|
||||
* returned {@code WebSocketPolicy} is read-only and changing it has no effect.
|
||||
* This method is a no-op for Jetty.
|
||||
*/
|
||||
@Override
|
||||
public void setBinaryMessageSizeLimit(int messageSizeLimit) {
|
||||
@@ -210,31 +210,57 @@ public class JettyWebSocketSession extends AbstractWebSocketSession<Session> {
|
||||
|
||||
@Override
|
||||
protected void sendTextMessage(TextMessage message) throws IOException {
|
||||
getRemoteEndpoint().sendString(message.getPayload());
|
||||
useSession((session, callback) -> session.sendText(message.getPayload(), callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendBinaryMessage(BinaryMessage message) throws IOException {
|
||||
getRemoteEndpoint().sendBytes(message.getPayload());
|
||||
useSession((session, callback) -> session.sendBinary(message.getPayload(), callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendPingMessage(PingMessage message) throws IOException {
|
||||
getRemoteEndpoint().sendPing(message.getPayload());
|
||||
useSession((session, callback) -> session.sendPing(message.getPayload(), callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendPongMessage(PongMessage message) throws IOException {
|
||||
getRemoteEndpoint().sendPong(message.getPayload());
|
||||
}
|
||||
|
||||
private RemoteEndpoint getRemoteEndpoint() {
|
||||
return getNativeSession().getRemote();
|
||||
useSession((session, callback) -> session.sendPong(message.getPayload(), callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeInternal(CloseStatus status) throws IOException {
|
||||
getNativeSession().close(status.getCode(), status.getReason());
|
||||
useSession((session, callback) -> session.close(status.getCode(), status.getReason(), callback));
|
||||
}
|
||||
|
||||
private void useSession(SessionConsumer sessionConsumer) throws IOException {
|
||||
try {
|
||||
Callback.Completable completable = new Callback.Completable();
|
||||
sessionConsumer.consume(getNativeSession(), completable);
|
||||
completable.get();
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
|
||||
if (cause instanceof IOException ioEx) {
|
||||
throw ioEx;
|
||||
}
|
||||
else if (cause instanceof UncheckedIOException uioEx) {
|
||||
throw uioEx.getCause();
|
||||
}
|
||||
else {
|
||||
throw new IOException(ex.getMessage(), cause);
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SessionConsumer {
|
||||
|
||||
void consume(Session session, Callback callback) throws IOException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* 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.socket.client.jetty;
|
||||
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.client.WebSocketClient;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.concurrent.FutureUtils;
|
||||
import org.springframework.web.socket.WebSocketExtension;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.adapter.jetty.JettyWebSocketHandlerAdapter;
|
||||
import org.springframework.web.socket.adapter.jetty.JettyWebSocketSession;
|
||||
import org.springframework.web.socket.adapter.jetty.WebSocketToJettyExtensionConfigAdapter;
|
||||
import org.springframework.web.socket.client.AbstractWebSocketClient;
|
||||
|
||||
/**
|
||||
* Initiates WebSocket requests to a WebSocket server programmatically
|
||||
* through the Jetty WebSocket API. Only supported on Jetty 11, superseded by
|
||||
* {@link org.springframework.web.socket.client.standard.StandardWebSocketClient}.
|
||||
*
|
||||
* <p>As of 4.1 this class implements {@link Lifecycle} rather than
|
||||
* {@link org.springframework.context.SmartLifecycle}. Use
|
||||
* {@link org.springframework.web.socket.client.WebSocketConnectionManager
|
||||
* WebSocketConnectionManager} instead to auto-start a WebSocket connection.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.0
|
||||
* @deprecated as of 6.0.3, in favor of
|
||||
* {@link org.springframework.web.socket.client.standard.StandardWebSocketClient}
|
||||
*/
|
||||
@Deprecated(since = "6.0.3", forRemoval = true)
|
||||
public class JettyWebSocketClient extends AbstractWebSocketClient implements Lifecycle {
|
||||
|
||||
private final org.eclipse.jetty.websocket.client.WebSocketClient client;
|
||||
|
||||
@Nullable
|
||||
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor that creates an instance of
|
||||
* {@link org.eclipse.jetty.websocket.client.WebSocketClient}.
|
||||
*/
|
||||
public JettyWebSocketClient() {
|
||||
this.client = new org.eclipse.jetty.websocket.client.WebSocketClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that accepts an existing
|
||||
* {@link org.eclipse.jetty.websocket.client.WebSocketClient} instance.
|
||||
*/
|
||||
public JettyWebSocketClient(WebSocketClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an {@link AsyncTaskExecutor} to use when opening connections.
|
||||
* <p>If this property is set to {@code null}, calls to any of the
|
||||
* {@code doHandshake} methods will block until the connection is established.
|
||||
* <p>By default an instance of {@code SimpleAsyncTaskExecutor} is used.
|
||||
*/
|
||||
public void setTaskExecutor(@Nullable AsyncTaskExecutor taskExecutor) {
|
||||
this.taskExecutor = taskExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link AsyncTaskExecutor}.
|
||||
*/
|
||||
@Nullable
|
||||
public AsyncTaskExecutor getTaskExecutor() {
|
||||
return this.taskExecutor;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
try {
|
||||
this.client.start();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Failed to start Jetty WebSocketClient", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
try {
|
||||
this.client.stop();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.error("Failed to stop Jetty WebSocketClient", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.client.isStarted();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CompletableFuture<WebSocketSession> executeInternal(WebSocketHandler wsHandler,
|
||||
HttpHeaders headers, final URI uri, List<String> protocols,
|
||||
List<WebSocketExtension> extensions, Map<String, Object> attributes) {
|
||||
|
||||
final ClientUpgradeRequest request = new ClientUpgradeRequest();
|
||||
request.setSubProtocols(protocols);
|
||||
|
||||
for (WebSocketExtension extension : extensions) {
|
||||
request.addExtensions(new WebSocketToJettyExtensionConfigAdapter(extension));
|
||||
}
|
||||
|
||||
request.setHeaders(headers);
|
||||
|
||||
Principal user = getUser();
|
||||
JettyWebSocketSession wsSession = new JettyWebSocketSession(attributes, user);
|
||||
|
||||
Callable<WebSocketSession> connectTask = () -> {
|
||||
JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter(wsHandler, wsSession);
|
||||
Future<Session> future = this.client.connect(adapter, uri, request);
|
||||
future.get(this.client.getConnectTimeout() + 2000, TimeUnit.MILLISECONDS);
|
||||
return wsSession;
|
||||
};
|
||||
|
||||
if (this.taskExecutor != null) {
|
||||
return FutureUtils.callAsync(connectTask, this.taskExecutor);
|
||||
}
|
||||
else {
|
||||
return FutureUtils.callAsync(connectTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user to make available through {@link WebSocketSession#getPrincipal()}.
|
||||
* By default, this method returns {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
protected Principal getUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Client-side support for the Jetty WebSocket API.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.web.socket.client.jetty;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -25,8 +25,8 @@ import java.util.Map;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketCreator;
|
||||
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
|
||||
@@ -23,11 +23,11 @@ import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.StringRequestContent;
|
||||
import org.eclipse.jetty.client.Request;
|
||||
import org.eclipse.jetty.client.Response;
|
||||
import org.eclipse.jetty.client.StringRequestContent;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
|
||||
@@ -186,11 +186,11 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle
|
||||
|
||||
|
||||
/**
|
||||
* Jetty client {@link org.eclipse.jetty.client.api.Response.Listener Response
|
||||
* Jetty client {@link org.eclipse.jetty.client.Response.Listener Response
|
||||
* Listener} that splits the body of the response into SockJS frames and
|
||||
* delegates them to the {@link XhrClientSockJsSession}.
|
||||
*/
|
||||
private class SockJsResponseListener extends Response.Listener.Adapter {
|
||||
private class SockJsResponseListener implements Response.Listener {
|
||||
|
||||
private final URI transportUrl;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -63,7 +63,6 @@ public abstract class AbstractWebSocketIntegrationTests {
|
||||
@SuppressWarnings("removal")
|
||||
static Stream<Arguments> argumentsFactory() {
|
||||
return Stream.of(
|
||||
arguments(named("Jetty", new JettyWebSocketTestServer()), named("Jetty", new org.springframework.web.socket.client.jetty.JettyWebSocketClient())),
|
||||
arguments(named("Tomcat", new TomcatWebSocketTestServer()), named("Standard", new StandardWebSocketClient())),
|
||||
arguments(named("Undertow", new UndertowTestServer()), named("Standard", new StandardWebSocketClient())));
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ import java.util.EnumSet;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import org.eclipse.jetty.ee10.servlet.FilterHolder;
|
||||
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.Connector;
|
||||
import org.eclipse.jetty.server.NetworkConnector;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
@@ -51,7 +51,7 @@ class JettyWebSocketHandlerAdapterTests {
|
||||
|
||||
@Test
|
||||
void onOpen() throws Exception {
|
||||
this.adapter.onWebSocketConnect(this.session);
|
||||
this.adapter.onWebSocketOpen(this.session);
|
||||
verify(this.webSocketHandler).afterConnectionEstablished(this.webSocketSession);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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.socket.client.jetty;
|
||||
|
||||
/**
|
||||
* Tests for {@link JettyWebSocketClient}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class JettyWebSocketClientTests {
|
||||
|
||||
/* TODO: complete upgrade to Jetty 11
|
||||
private JettyWebSocketClient client;
|
||||
|
||||
private TestJettyWebSocketServer server;
|
||||
|
||||
private String wsUrl;
|
||||
|
||||
private WebSocketSession wsSession;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
|
||||
this.server = new TestJettyWebSocketServer(new TextWebSocketHandler());
|
||||
this.server.start();
|
||||
|
||||
this.client = new JettyWebSocketClient();
|
||||
this.client.start();
|
||||
|
||||
this.wsUrl = "ws://localhost:" + this.server.getPort() + "/test";
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() throws Exception {
|
||||
this.wsSession.close();
|
||||
this.client.stop();
|
||||
this.server.stop();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void doHandshake() throws Exception {
|
||||
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.setSecWebSocketProtocol(Arrays.asList("echo"));
|
||||
|
||||
this.wsSession = this.client.doHandshake(new TextWebSocketHandler(), headers, new URI(this.wsUrl)).get();
|
||||
|
||||
assertThat(this.wsSession.getUri().toString()).isEqualTo(this.wsUrl);
|
||||
assertThat(this.wsSession.getAcceptedProtocol()).isEqualTo("echo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doHandshakeWithTaskExecutor() throws Exception {
|
||||
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.setSecWebSocketProtocol(Arrays.asList("echo"));
|
||||
|
||||
this.client.setTaskExecutor(new SimpleAsyncTaskExecutor());
|
||||
this.wsSession = this.client.doHandshake(new TextWebSocketHandler(), headers, new URI(this.wsUrl)).get();
|
||||
|
||||
assertThat(this.wsSession.getUri().toString()).isEqualTo(this.wsUrl);
|
||||
assertThat(this.wsSession.getAcceptedProtocol()).isEqualTo("echo");
|
||||
}
|
||||
|
||||
|
||||
private static class TestJettyWebSocketServer {
|
||||
|
||||
private final Server server;
|
||||
|
||||
|
||||
public TestJettyWebSocketServer(final WebSocketHandler webSocketHandler) {
|
||||
|
||||
this.server = new Server();
|
||||
ServerConnector connector = new ServerConnector(this.server);
|
||||
connector.setPort(0);
|
||||
|
||||
this.server.addConnector(connector);
|
||||
this.server.setHandler(new WebSocketUpgradeHandler() {
|
||||
@Override
|
||||
public void configure(JettyWebSocketServletFactory factory) {
|
||||
factory.setCreator(new JettyWebSocketCreator() {
|
||||
@Override
|
||||
public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) {
|
||||
if (!CollectionUtils.isEmpty(req.getSubProtocols())) {
|
||||
resp.setAcceptedSubProtocol(req.getSubProtocols().get(0));
|
||||
}
|
||||
JettyWebSocketSession session = new JettyWebSocketSession(null, null);
|
||||
return new JettyWebSocketHandlerAdapter(webSocketHandler, session);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void start() throws Exception {
|
||||
this.server.start();
|
||||
}
|
||||
|
||||
public void stop() throws Exception {
|
||||
this.server.stop();
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return ((ServerConnector) this.server.getConnectors()[0]).getLocalPort();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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.socket.sockjs.client;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.JettyWebSocketTestServer;
|
||||
import org.springframework.web.socket.server.RequestUpgradeStrategy;
|
||||
import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy;
|
||||
|
||||
/**
|
||||
* SockJS integration tests using Jetty for client and server.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
class JettySockJsIntegrationTests extends AbstractSockJsIntegrationTests {
|
||||
|
||||
@Override
|
||||
protected Class<?> upgradeStrategyConfigClass() {
|
||||
return JettyTestConfig.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JettyWebSocketTestServer createWebSocketTestServer() {
|
||||
return new JettyWebSocketTestServer();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Override
|
||||
protected Transport createWebSocketTransport() {
|
||||
return new WebSocketTransport(new org.springframework.web.socket.client.jetty.JettyWebSocketClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractXhrTransport createXhrTransport() {
|
||||
return new JettyXhrTransport(new HttpClient());
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class JettyTestConfig {
|
||||
@Bean
|
||||
RequestUpgradeStrategy upgradeStrategy() {
|
||||
return new JettyRequestUpgradeStrategy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user