Browse Source
This provides an implementation of an HTTP Handler Adapter that is coded directly to the Eclipse Jetty core API, bypassing any servlet implementation. This includes a Jetty implementation of the spring `WebSocketClient` interface, `JettyWebSocketClient`, using an explicit dependency to the jetty-websocket-api. Closes gh-32097 Co-authored-by: Lachlan Roberts <lachlan@webtide.com> Co-authored-by: Arjen Poutsma <arjen.poutsma@broadcom.com>pull/33181/head
33 changed files with 1692 additions and 517 deletions
@ -0,0 +1,359 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.io.buffer; |
||||||
|
|
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.nio.charset.Charset; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
import java.util.function.IntPredicate; |
||||||
|
|
||||||
|
import org.eclipse.jetty.io.Content; |
||||||
|
|
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implementation of the {@code DataBuffer} interface that can wrap a Jetty |
||||||
|
* {@link Content.Chunk}. Typically constructed with {@link JettyDataBufferFactory}. |
||||||
|
* |
||||||
|
* @author Greg Wilkins |
||||||
|
* @author Lachlan Roberts |
||||||
|
* @author Arjen Poutsma |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
public final class JettyDataBuffer implements PooledDataBuffer { |
||||||
|
|
||||||
|
private final DefaultDataBuffer delegate; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Content.Chunk chunk; |
||||||
|
|
||||||
|
private final JettyDataBufferFactory bufferFactory; |
||||||
|
|
||||||
|
private final AtomicInteger refCount = new AtomicInteger(1); |
||||||
|
|
||||||
|
|
||||||
|
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate, Content.Chunk chunk) { |
||||||
|
Assert.notNull(bufferFactory, "BufferFactory must not be null"); |
||||||
|
Assert.notNull(delegate, "Delegate must not be null"); |
||||||
|
Assert.notNull(chunk, "Chunk must not be null"); |
||||||
|
|
||||||
|
this.bufferFactory = bufferFactory; |
||||||
|
this.delegate = delegate; |
||||||
|
this.chunk = chunk; |
||||||
|
this.chunk.retain(); |
||||||
|
} |
||||||
|
|
||||||
|
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) { |
||||||
|
Assert.notNull(bufferFactory, "BufferFactory must not be null"); |
||||||
|
Assert.notNull(delegate, "Delegate must not be null"); |
||||||
|
|
||||||
|
this.bufferFactory = bufferFactory; |
||||||
|
this.delegate = delegate; |
||||||
|
this.chunk = null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isAllocated() { |
||||||
|
return this.refCount.get() > 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PooledDataBuffer retain() { |
||||||
|
int result = this.refCount.updateAndGet(c -> { |
||||||
|
if (c != 0) { |
||||||
|
return c + 1; |
||||||
|
} |
||||||
|
else { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}); |
||||||
|
if (result != 0 && this.chunk != null) { |
||||||
|
this.chunk.retain(); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PooledDataBuffer touch(Object hint) { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean release() { |
||||||
|
int result = this.refCount.updateAndGet(c -> { |
||||||
|
if (c != 0) { |
||||||
|
return c - 1; |
||||||
|
} |
||||||
|
else { |
||||||
|
throw new IllegalStateException("JettyDataBuffer already released: " + this); |
||||||
|
} |
||||||
|
}); |
||||||
|
if (this.chunk != null) { |
||||||
|
return this.chunk.release(); |
||||||
|
} |
||||||
|
else { |
||||||
|
return result == 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBufferFactory factory() { |
||||||
|
return this.bufferFactory; |
||||||
|
} |
||||||
|
|
||||||
|
// delegation
|
||||||
|
|
||||||
|
@Override |
||||||
|
public int indexOf(IntPredicate predicate, int fromIndex) { |
||||||
|
return this.delegate.indexOf(predicate, fromIndex); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int lastIndexOf(IntPredicate predicate, int fromIndex) { |
||||||
|
return this.delegate.lastIndexOf(predicate, fromIndex); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int readableByteCount() { |
||||||
|
return this.delegate.readableByteCount(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int writableByteCount() { |
||||||
|
return this.delegate.writableByteCount(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int capacity() { |
||||||
|
return this.delegate.capacity(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public DataBuffer capacity(int capacity) { |
||||||
|
this.delegate.capacity(capacity); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer ensureWritable(int capacity) { |
||||||
|
this.delegate.ensureWritable(capacity); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int readPosition() { |
||||||
|
return this.delegate.readPosition(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer readPosition(int readPosition) { |
||||||
|
this.delegate.readPosition(readPosition); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int writePosition() { |
||||||
|
return this.delegate.writePosition(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer writePosition(int writePosition) { |
||||||
|
this.delegate.writePosition(writePosition); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte getByte(int index) { |
||||||
|
return this.delegate.getByte(index); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public byte read() { |
||||||
|
return this.delegate.read(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer read(byte[] destination) { |
||||||
|
this.delegate.read(destination); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer read(byte[] destination, int offset, int length) { |
||||||
|
this.delegate.read(destination, offset, length); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer write(byte b) { |
||||||
|
this.delegate.write(b); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer write(byte[] source) { |
||||||
|
this.delegate.write(source); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer write(byte[] source, int offset, int length) { |
||||||
|
this.delegate.write(source, offset, length); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer write(DataBuffer... buffers) { |
||||||
|
this.delegate.write(buffers); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer write(ByteBuffer... buffers) { |
||||||
|
this.delegate.write(buffers); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public DataBuffer slice(int index, int length) { |
||||||
|
DefaultDataBuffer delegateSlice = this.delegate.slice(index, length); |
||||||
|
if (this.chunk != null) { |
||||||
|
this.chunk.retain(); |
||||||
|
return new JettyDataBuffer(this.bufferFactory, delegateSlice, this.chunk); |
||||||
|
} |
||||||
|
else { |
||||||
|
return new JettyDataBuffer(this.bufferFactory, delegateSlice); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DataBuffer split(int index) { |
||||||
|
DefaultDataBuffer delegateSplit = this.delegate.split(index); |
||||||
|
if (this.chunk != null) { |
||||||
|
this.chunk.retain(); |
||||||
|
return new JettyDataBuffer(this.bufferFactory, delegateSplit, this.chunk); |
||||||
|
} |
||||||
|
else { |
||||||
|
return new JettyDataBuffer(this.bufferFactory, delegateSplit); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public ByteBuffer asByteBuffer() { |
||||||
|
return this.delegate.asByteBuffer(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public ByteBuffer asByteBuffer(int index, int length) { |
||||||
|
return this.delegate.asByteBuffer(index, length); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public ByteBuffer toByteBuffer(int index, int length) { |
||||||
|
return this.delegate.toByteBuffer(index, length); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { |
||||||
|
this.delegate.toByteBuffer(srcPos, dest, destPos, length); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ByteBufferIterator readableByteBuffers() { |
||||||
|
ByteBufferIterator delegateIterator = this.delegate.readableByteBuffers(); |
||||||
|
if (this.chunk != null) { |
||||||
|
return new JettyByteBufferIterator(delegateIterator, this.chunk); |
||||||
|
} |
||||||
|
else { |
||||||
|
return delegateIterator; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ByteBufferIterator writableByteBuffers() { |
||||||
|
ByteBufferIterator delegateIterator = this.delegate.writableByteBuffers(); |
||||||
|
if (this.chunk != null) { |
||||||
|
return new JettyByteBufferIterator(delegateIterator, this.chunk); |
||||||
|
} |
||||||
|
else { |
||||||
|
return delegateIterator; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString(int index, int length, Charset charset) { |
||||||
|
return this.delegate.toString(index, length, charset); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int hashCode() { |
||||||
|
return this.delegate.hashCode(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(Object o) { |
||||||
|
return this == o || (o instanceof JettyDataBuffer other && |
||||||
|
this.delegate.equals(other.delegate)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return String.format("JettyDataBuffer (r: %d, w: %d, c: %d)", |
||||||
|
readPosition(), writePosition(), capacity()); |
||||||
|
} |
||||||
|
|
||||||
|
private static final class JettyByteBufferIterator implements ByteBufferIterator { |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.io.buffer; |
||||||
|
|
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.eclipse.jetty.io.Content; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implementation of the {@code DataBufferFactory} interface that creates |
||||||
|
* {@link JettyDataBuffer} instances. |
||||||
|
* |
||||||
|
* @author Arjen Poutsma |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
public class JettyDataBufferFactory implements DataBufferFactory { |
||||||
|
|
||||||
|
private final DefaultDataBufferFactory delegate; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@code JettyDataBufferFactory} with default settings. |
||||||
|
*/ |
||||||
|
public JettyDataBufferFactory() { |
||||||
|
this(false); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@code JettyDataBufferFactory}, indicating whether direct |
||||||
|
* buffers should be created by {@link #allocateBuffer()} and |
||||||
|
* {@link #allocateBuffer(int)}. |
||||||
|
* @param preferDirect {@code true} if direct buffers are to be preferred; |
||||||
|
* {@code false} otherwise |
||||||
|
*/ |
||||||
|
public JettyDataBufferFactory(boolean preferDirect) { |
||||||
|
this(preferDirect, DefaultDataBufferFactory.DEFAULT_INITIAL_CAPACITY); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@code JettyDataBufferFactory}, indicating whether direct |
||||||
|
* buffers should be created by {@link #allocateBuffer()} and |
||||||
|
* {@link #allocateBuffer(int)}, and what the capacity is to be used for |
||||||
|
* {@link #allocateBuffer()}. |
||||||
|
* @param preferDirect {@code true} if direct buffers are to be preferred; |
||||||
|
* {@code false} otherwise |
||||||
|
*/ |
||||||
|
public JettyDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { |
||||||
|
this.delegate = new DefaultDataBufferFactory(preferDirect, defaultInitialCapacity); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
@Deprecated |
||||||
|
public JettyDataBuffer allocateBuffer() { |
||||||
|
DefaultDataBuffer delegate = this.delegate.allocateBuffer(); |
||||||
|
return new JettyDataBuffer(this, delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public JettyDataBuffer allocateBuffer(int initialCapacity) { |
||||||
|
DefaultDataBuffer delegate = this.delegate.allocateBuffer(initialCapacity); |
||||||
|
return new JettyDataBuffer(this, delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public JettyDataBuffer wrap(ByteBuffer byteBuffer) { |
||||||
|
DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer); |
||||||
|
return new JettyDataBuffer(this, delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public JettyDataBuffer wrap(byte[] bytes) { |
||||||
|
DefaultDataBuffer delegate = this.delegate.wrap(bytes); |
||||||
|
return new JettyDataBuffer(this, delegate); |
||||||
|
} |
||||||
|
|
||||||
|
public JettyDataBuffer wrap(Content.Chunk chunk) { |
||||||
|
ByteBuffer byteBuffer = chunk.getByteBuffer(); |
||||||
|
DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer); |
||||||
|
return new JettyDataBuffer(this, delegate, chunk); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public JettyDataBuffer join(List<? extends DataBuffer> dataBuffers) { |
||||||
|
DefaultDataBuffer delegate = this.delegate.join(dataBuffers); |
||||||
|
return new JettyDataBuffer(this, delegate); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isDirect() { |
||||||
|
return this.delegate.isDirect(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.core.io.buffer; |
||||||
|
|
||||||
|
import java.nio.ByteBuffer; |
||||||
|
|
||||||
|
import org.eclipse.jetty.io.Content; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||||
|
import static org.mockito.BDDMockito.given; |
||||||
|
import static org.mockito.BDDMockito.then; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Arjen Poutsma |
||||||
|
*/ |
||||||
|
public class JettyDataBufferTests { |
||||||
|
|
||||||
|
private final JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory(); |
||||||
|
|
||||||
|
@Test |
||||||
|
void releaseRetainChunk() { |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(3); |
||||||
|
Content.Chunk mockChunk = mock(); |
||||||
|
given(mockChunk.getByteBuffer()).willReturn(buffer); |
||||||
|
given(mockChunk.release()).willReturn(false, false, true); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(mockChunk); |
||||||
|
dataBuffer.retain(); |
||||||
|
dataBuffer.retain(); |
||||||
|
assertThat(dataBuffer.release()).isFalse(); |
||||||
|
assertThat(dataBuffer.release()).isFalse(); |
||||||
|
assertThat(dataBuffer.release()).isTrue(); |
||||||
|
|
||||||
|
assertThatIllegalStateException().isThrownBy(dataBuffer::release); |
||||||
|
|
||||||
|
then(mockChunk).should(times(3)).retain(); |
||||||
|
then(mockChunk).should(times(3)).release(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.http.server.reactive; |
||||||
|
|
||||||
|
import org.eclipse.jetty.server.Handler; |
||||||
|
import org.eclipse.jetty.server.Request; |
||||||
|
import org.eclipse.jetty.server.Response; |
||||||
|
import org.eclipse.jetty.util.Callback; |
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.JettyDataBufferFactory; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* Adapt {@link HttpHandler} to the Jetty {@link org.eclipse.jetty.server.Handler} abstraction. |
||||||
|
* |
||||||
|
* @author Greg Wilkins |
||||||
|
* @author Lachlan Roberts |
||||||
|
* @author Arjen Poutsma |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
public class JettyCoreHttpHandlerAdapter extends Handler.Abstract.NonBlocking { |
||||||
|
|
||||||
|
private final HttpHandler httpHandler; |
||||||
|
|
||||||
|
private JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory(); |
||||||
|
|
||||||
|
|
||||||
|
public JettyCoreHttpHandlerAdapter(HttpHandler httpHandler) { |
||||||
|
this.httpHandler = httpHandler; |
||||||
|
} |
||||||
|
|
||||||
|
public void setDataBufferFactory(JettyDataBufferFactory dataBufferFactory) { |
||||||
|
Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); |
||||||
|
this.dataBufferFactory = dataBufferFactory; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean handle(Request request, Response response, Callback callback) throws Exception { |
||||||
|
this.httpHandler.handle(new JettyCoreServerHttpRequest(request, this.dataBufferFactory), |
||||||
|
new JettyCoreServerHttpResponse(response, this.dataBufferFactory)) |
||||||
|
.subscribe(unused -> {}, callback::failed, callback::succeeded); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,120 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.http.server.reactive; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.net.SocketAddress; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.eclipse.jetty.io.Content; |
||||||
|
import org.eclipse.jetty.io.EndPoint; |
||||||
|
import org.eclipse.jetty.server.Request; |
||||||
|
import org.reactivestreams.FlowAdapters; |
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer; |
||||||
|
import org.springframework.core.io.buffer.JettyDataBufferFactory; |
||||||
|
import org.springframework.http.HttpCookie; |
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.support.JettyHeadersAdapter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.util.CollectionUtils; |
||||||
|
import org.springframework.util.LinkedMultiValueMap; |
||||||
|
import org.springframework.util.MultiValueMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* Adapt an Eclipse Jetty {@link Request} to a {@link org.springframework.http.server.ServerHttpRequest}. |
||||||
|
* |
||||||
|
* @author Greg Wilkins |
||||||
|
* @author Arjen Poutsma |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
class JettyCoreServerHttpRequest extends AbstractServerHttpRequest { |
||||||
|
|
||||||
|
private final JettyDataBufferFactory dataBufferFactory; |
||||||
|
|
||||||
|
private final Request request; |
||||||
|
|
||||||
|
|
||||||
|
public JettyCoreServerHttpRequest(Request request, JettyDataBufferFactory dataBufferFactory) { |
||||||
|
super(HttpMethod.valueOf(request.getMethod()), |
||||||
|
request.getHttpURI().toURI(), |
||||||
|
request.getContext().getContextPath(), |
||||||
|
new HttpHeaders(new JettyHeadersAdapter(request.getHeaders()))); |
||||||
|
this.dataBufferFactory = dataBufferFactory; |
||||||
|
this.request = request; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MultiValueMap<String, HttpCookie> initCookies() { |
||||||
|
List<org.eclipse.jetty.http.HttpCookie> httpCookies = Request.getCookies(this.request); |
||||||
|
if (httpCookies.isEmpty()) { |
||||||
|
return CollectionUtils.toMultiValueMap(Collections.emptyMap()); |
||||||
|
} |
||||||
|
MultiValueMap<String, HttpCookie> cookies =new LinkedMultiValueMap<>(); |
||||||
|
for (org.eclipse.jetty.http.HttpCookie c : httpCookies) { |
||||||
|
cookies.add(c.getName(), new HttpCookie(c.getName(), c.getValue())); |
||||||
|
} |
||||||
|
return cookies; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public SslInfo initSslInfo() { |
||||||
|
if (this.request.getConnectionMetaData().isSecure() && |
||||||
|
this.request.getAttribute(EndPoint.SslSessionData.ATTRIBUTE) instanceof EndPoint.SslSessionData sessionData) { |
||||||
|
return new DefaultSslInfo(sessionData.sslSessionId(), sessionData.peerCertificates()); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Override |
||||||
|
public <T> T getNativeRequest() { |
||||||
|
return (T) this.request; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String initId() { |
||||||
|
return this.request.getId(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public InetSocketAddress getLocalAddress() { |
||||||
|
SocketAddress localAddress = this.request.getConnectionMetaData().getLocalSocketAddress(); |
||||||
|
return localAddress instanceof InetSocketAddress inet ? inet : null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public InetSocketAddress getRemoteAddress() { |
||||||
|
SocketAddress remoteAddress = this.request.getConnectionMetaData().getRemoteSocketAddress(); |
||||||
|
return remoteAddress instanceof InetSocketAddress inet ? inet : null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Flux<DataBuffer> getBody() { |
||||||
|
// We access the request body as a Flow.Publisher, which is wrapped as an org.reactivestreams.Publisher and
|
||||||
|
// then wrapped as a Flux.
|
||||||
|
return Flux.from(FlowAdapters.toPublisher(Content.Source.asPublisher(this.request))) |
||||||
|
.map(this.dataBufferFactory::wrap); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,237 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2024 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.http.server.reactive; |
||||||
|
|
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.ListIterator; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.eclipse.jetty.http.HttpCookie; |
||||||
|
import org.eclipse.jetty.http.HttpField; |
||||||
|
import org.eclipse.jetty.io.Content; |
||||||
|
import org.eclipse.jetty.server.HttpCookieUtils; |
||||||
|
import org.eclipse.jetty.server.Response; |
||||||
|
import org.eclipse.jetty.util.Callback; |
||||||
|
import org.eclipse.jetty.util.IteratingCallback; |
||||||
|
import org.reactivestreams.Publisher; |
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer; |
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils; |
||||||
|
import org.springframework.core.io.buffer.JettyDataBufferFactory; |
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.http.HttpStatusCode; |
||||||
|
import org.springframework.http.ResponseCookie; |
||||||
|
import org.springframework.http.ZeroCopyHttpOutputMessage; |
||||||
|
import org.springframework.http.support.JettyHeadersAdapter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Adapt an Eclipse Jetty {@link Response} to a {@link org.springframework.http.server.ServerHttpResponse}. |
||||||
|
* |
||||||
|
* @author Greg Wilkins |
||||||
|
* @author Lachlan Roberts |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { |
||||||
|
|
||||||
|
private final Response response; |
||||||
|
|
||||||
|
public JettyCoreServerHttpResponse(Response response, JettyDataBufferFactory dataBufferFactory) { |
||||||
|
super(dataBufferFactory, new HttpHeaders(new JettyHeadersAdapter(response.getHeaders()))); |
||||||
|
this.response = response; |
||||||
|
|
||||||
|
// remove all existing cookies from the response and add them to the cookie map, to be added back later
|
||||||
|
for (ListIterator<HttpField> i = this.response.getHeaders().listIterator(); i.hasNext(); ) { |
||||||
|
HttpField f = i.next(); |
||||||
|
if (f instanceof HttpCookieUtils.SetCookieHttpField setCookieHttpField) { |
||||||
|
HttpCookie httpCookie = setCookieHttpField.getHttpCookie(); |
||||||
|
ResponseCookie responseCookie = ResponseCookie.from(httpCookie.getName(), httpCookie.getValue()) |
||||||
|
.httpOnly(httpCookie.isHttpOnly()) |
||||||
|
.domain(httpCookie.getDomain()) |
||||||
|
.maxAge(httpCookie.getMaxAge()) |
||||||
|
.sameSite(httpCookie.getSameSite().name()) |
||||||
|
.secure(httpCookie.isSecure()) |
||||||
|
.partitioned(httpCookie.isPartitioned()) |
||||||
|
.build(); |
||||||
|
this.addCookie(responseCookie); |
||||||
|
i.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Mono<Void> writeWithInternal(Publisher<? extends DataBuffer> body) { |
||||||
|
return Flux.from(body) |
||||||
|
.concatMap(this::sendDataBuffer) |
||||||
|
.then(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Mono<Void> writeAndFlushWithInternal(Publisher<? extends Publisher<? extends DataBuffer>> body) { |
||||||
|
return Flux.from(body).concatMap(this::writeWithInternal).then(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void applyStatusCode() { |
||||||
|
HttpStatusCode status = getStatusCode(); |
||||||
|
this.response.setStatus(status == null ? 0 : status.value()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void applyHeaders() { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void applyCookies() { |
||||||
|
this.getCookies().values().stream() |
||||||
|
.flatMap(List::stream) |
||||||
|
.forEach(cookie -> Response.addCookie(this.response, new ResponseHttpCookie(cookie))); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> writeWith(Path file, long position, long count) { |
||||||
|
Callback.Completable callback = new Callback.Completable(); |
||||||
|
Mono<Void> mono = Mono.fromFuture(callback); |
||||||
|
try { |
||||||
|
Content.copy(Content.Source.from(null, file, position, count), this.response, callback); |
||||||
|
} |
||||||
|
catch (Throwable th) { |
||||||
|
callback.failed(th); |
||||||
|
} |
||||||
|
return doCommit(() -> mono); |
||||||
|
} |
||||||
|
|
||||||
|
private Mono<Void> sendDataBuffer(DataBuffer dataBuffer) { |
||||||
|
return Mono.defer(() -> { |
||||||
|
DataBuffer.ByteBufferIterator byteBufferIterator = dataBuffer.readableByteBuffers(); |
||||||
|
Callback.Completable callback = new Callback.Completable(); |
||||||
|
new IteratingCallback() { |
||||||
|
@Override |
||||||
|
protected Action process() { |
||||||
|
if (!byteBufferIterator.hasNext()) { |
||||||
|
return Action.SUCCEEDED; |
||||||
|
} |
||||||
|
response.write(false, byteBufferIterator.next(), this); |
||||||
|
return Action.SCHEDULED; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onCompleteSuccess() { |
||||||
|
byteBufferIterator.close(); |
||||||
|
DataBufferUtils.release(dataBuffer); |
||||||
|
callback.complete(null); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onCompleteFailure(Throwable cause) { |
||||||
|
byteBufferIterator.close(); |
||||||
|
DataBufferUtils.release(dataBuffer); |
||||||
|
callback.failed(cause); |
||||||
|
} |
||||||
|
}.iterate(); |
||||||
|
|
||||||
|
return Mono.fromFuture(callback); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Override |
||||||
|
public <T> T getNativeResponse() { |
||||||
|
return (T) this.response; |
||||||
|
} |
||||||
|
|
||||||
|
private static class ResponseHttpCookie implements org.eclipse.jetty.http.HttpCookie { |
||||||
|
private final ResponseCookie responseCookie; |
||||||
|
|
||||||
|
public ResponseHttpCookie(ResponseCookie responseCookie) { |
||||||
|
this.responseCookie = responseCookie; |
||||||
|
} |
||||||
|
|
||||||
|
public ResponseCookie getResponseCookie() { |
||||||
|
return this.responseCookie; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getName() { |
||||||
|
return this.responseCookie.getName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getValue() { |
||||||
|
return this.responseCookie.getValue(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getVersion() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public long getMaxAge() { |
||||||
|
return this.responseCookie.getMaxAge().toSeconds(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String getComment() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String getDomain() { |
||||||
|
return this.responseCookie.getDomain(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@Nullable |
||||||
|
public String getPath() { |
||||||
|
return this.responseCookie.getPath(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isSecure() { |
||||||
|
return this.responseCookie.isSecure(); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
@Override |
||||||
|
public SameSite getSameSite() { |
||||||
|
// Adding non-null return site breaks tests.
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isHttpOnly() { |
||||||
|
return this.responseCookie.isHttpOnly(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isPartitioned() { |
||||||
|
return this.responseCookie.isPartitioned(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, String> getAttributes() { |
||||||
|
return Collections.emptyMap(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2023 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.testfixture.http.server.reactive.bootstrap; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import org.eclipse.jetty.io.ArrayByteBufferPool; |
||||||
|
import org.eclipse.jetty.server.Server; |
||||||
|
import org.eclipse.jetty.server.ServerConnector; |
||||||
|
import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; |
||||||
|
|
||||||
|
import org.springframework.http.server.reactive.JettyCoreHttpHandlerAdapter; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @author Sam Brannen |
||||||
|
* @author Greg Wilkins |
||||||
|
* @since 6.2 |
||||||
|
*/ |
||||||
|
public class JettyCoreHttpServer extends AbstractHttpServer { |
||||||
|
|
||||||
|
protected Log logger = LogFactory.getLog(getClass().getName()); |
||||||
|
|
||||||
|
private ArrayByteBufferPool byteBufferPool; |
||||||
|
|
||||||
|
private Server jettyServer; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void initServer() { |
||||||
|
if (logger.isTraceEnabled()) |
||||||
|
this.byteBufferPool = new ArrayByteBufferPool.Tracking(); |
||||||
|
this.jettyServer = new Server(null, null, byteBufferPool); |
||||||
|
|
||||||
|
ServerConnector connector = new ServerConnector(this.jettyServer); |
||||||
|
connector.setHost(getHost()); |
||||||
|
connector.setPort(getPort()); |
||||||
|
this.jettyServer.addConnector(connector); |
||||||
|
this.jettyServer.setHandler(createHandlerAdapter()); |
||||||
|
|
||||||
|
ServerWebSocketContainer.ensure(jettyServer); |
||||||
|
} |
||||||
|
|
||||||
|
private JettyCoreHttpHandlerAdapter createHandlerAdapter() { |
||||||
|
return new JettyCoreHttpHandlerAdapter(resolveHttpHandler()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void startInternal() throws Exception { |
||||||
|
this.jettyServer.start(); |
||||||
|
setPort(((ServerConnector) this.jettyServer.getConnectors()[0]).getLocalPort()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void stopInternal() { |
||||||
|
boolean wasRunning = this.jettyServer.isRunning(); |
||||||
|
try { |
||||||
|
this.jettyServer.stop(); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
// ignore
|
||||||
|
} |
||||||
|
|
||||||
|
// TODO remove this or make debug only
|
||||||
|
if (wasRunning && this.byteBufferPool instanceof ArrayByteBufferPool.Tracking tracking) { |
||||||
|
if (!tracking.getLeaks().isEmpty()) { |
||||||
|
System.err.println("Leaks:\n" + tracking.dumpLeaks()); |
||||||
|
throw new IllegalStateException("LEAKS"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void resetInternal() { |
||||||
|
try { |
||||||
|
if (this.jettyServer.isRunning()) { |
||||||
|
stopInternal(); |
||||||
|
} |
||||||
|
this.jettyServer.destroy(); |
||||||
|
} |
||||||
|
finally { |
||||||
|
this.jettyServer = null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,111 @@ |
|||||||
|
/* |
||||||
|
* 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.Objects; |
||||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||||
|
|
||||||
|
import org.eclipse.jetty.client.Request; |
||||||
|
import org.eclipse.jetty.client.Response; |
||||||
|
import org.eclipse.jetty.http.HttpHeader; |
||||||
|
import org.eclipse.jetty.util.component.LifeCycle; |
||||||
|
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; |
||||||
|
import org.eclipse.jetty.websocket.client.JettyUpgradeListener; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
import reactor.core.publisher.Sinks; |
||||||
|
|
||||||
|
import org.springframework.context.Lifecycle; |
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory; |
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.web.reactive.socket.HandshakeInfo; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; |
||||||
|
import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; |
||||||
|
|
||||||
|
public class JettyWebSocketClient implements WebSocketClient, Lifecycle { |
||||||
|
|
||||||
|
private final org.eclipse.jetty.websocket.client.WebSocketClient client; |
||||||
|
|
||||||
|
public JettyWebSocketClient() { |
||||||
|
this(new org.eclipse.jetty.websocket.client.WebSocketClient()); |
||||||
|
} |
||||||
|
|
||||||
|
public JettyWebSocketClient(org.eclipse.jetty.websocket.client.WebSocketClient client) { |
||||||
|
this.client = client; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void start() { |
||||||
|
LifeCycle.start(this.client); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void stop() { |
||||||
|
LifeCycle.stop(this.client); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isRunning() { |
||||||
|
return this.client.isRunning(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> execute(URI url, WebSocketHandler handler) { |
||||||
|
return execute(url, null, handler); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> execute(URI url, @Nullable HttpHeaders headers, WebSocketHandler handler) { |
||||||
|
|
||||||
|
ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); |
||||||
|
upgradeRequest.setSubProtocols(handler.getSubProtocols()); |
||||||
|
if (headers != null) { |
||||||
|
headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); |
||||||
|
} |
||||||
|
|
||||||
|
final AtomicReference<HandshakeInfo> handshakeInfo = new AtomicReference<>(); |
||||||
|
JettyUpgradeListener jettyUpgradeListener = new JettyUpgradeListener() { |
||||||
|
@Override |
||||||
|
public void onHandshakeResponse(Request request, Response response) { |
||||||
|
String protocol = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); |
||||||
|
HttpHeaders responseHeaders = new HttpHeaders(); |
||||||
|
response.getHeaders().forEach(header -> responseHeaders.add(header.getName(), header.getValue())); |
||||||
|
handshakeInfo.set(new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol)); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
Sinks.Empty<Void> completion = Sinks.empty(); |
||||||
|
JettyWebSocketHandlerAdapter handlerAdapter = new JettyWebSocketHandlerAdapter(handler, session -> |
||||||
|
new JettyWebSocketSession(session, Objects.requireNonNull(handshakeInfo.get()), DefaultDataBufferFactory.sharedInstance, completion)); |
||||||
|
try { |
||||||
|
this.client.connect(handlerAdapter, url, upgradeRequest, jettyUpgradeListener) |
||||||
|
.exceptionally(throwable -> { |
||||||
|
// Only fail the completion if we have an error
|
||||||
|
// as the JettyWebSocketSession will never be opened.
|
||||||
|
completion.tryEmitError(throwable); |
||||||
|
return null; |
||||||
|
}); |
||||||
|
return completion.asMono(); |
||||||
|
} |
||||||
|
catch (IOException ex) { |
||||||
|
return Mono.error(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,127 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2023 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.web.reactive.socket.server.upgrade; |
||||||
|
|
||||||
|
import java.util.function.Consumer; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; |
||||||
|
import org.eclipse.jetty.server.Request; |
||||||
|
import org.eclipse.jetty.server.Response; |
||||||
|
import org.eclipse.jetty.server.Server; |
||||||
|
import org.eclipse.jetty.util.Callback; |
||||||
|
import org.eclipse.jetty.websocket.api.Configurable; |
||||||
|
import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; |
||||||
|
import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; |
||||||
|
import org.eclipse.jetty.websocket.server.WebSocketCreator; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.web.reactive.socket.HandshakeInfo; |
||||||
|
import org.springframework.web.reactive.socket.WebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; |
||||||
|
import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; |
||||||
|
import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; |
||||||
|
import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* A WebSocket {@code RequestUpgradeStrategy} for Jetty 12 Core. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.3.4 |
||||||
|
*/ |
||||||
|
public class JettyCoreRequestUpgradeStrategy implements RequestUpgradeStrategy { |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Consumer<Configurable> webSocketConfigurer; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private ServerWebSocketContainer serverContainer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Add a callback to configure WebSocket server parameters on |
||||||
|
* {@link JettyWebSocketServerContainer}. |
||||||
|
* @since 6.1 |
||||||
|
*/ |
||||||
|
public void addWebSocketConfigurer(Consumer<Configurable> webSocketConfigurer) { |
||||||
|
this.webSocketConfigurer = (this.webSocketConfigurer != null ? |
||||||
|
this.webSocketConfigurer.andThen(webSocketConfigurer) : webSocketConfigurer); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> upgrade( |
||||||
|
ServerWebExchange exchange, WebSocketHandler handler, |
||||||
|
@Nullable String subProtocol, Supplier<HandshakeInfo> handshakeInfoFactory) { |
||||||
|
|
||||||
|
ServerHttpRequest request = exchange.getRequest(); |
||||||
|
ServerHttpResponse response = exchange.getResponse(); |
||||||
|
|
||||||
|
Request jettyRequest = ServerHttpRequestDecorator.getNativeRequest(request); |
||||||
|
Response jettyResponse = ServerHttpResponseDecorator.getNativeResponse(response); |
||||||
|
|
||||||
|
HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); |
||||||
|
DataBufferFactory factory = response.bufferFactory(); |
||||||
|
|
||||||
|
// Trigger WebFlux preCommit actions before upgrade
|
||||||
|
return exchange.getResponse().setComplete() |
||||||
|
.then(Mono.deferContextual(contextView -> { |
||||||
|
JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( |
||||||
|
ContextWebSocketHandler.decorate(handler, contextView), |
||||||
|
session -> new JettyWebSocketSession(session, handshakeInfo, factory)); |
||||||
|
|
||||||
|
WebSocketCreator webSocketCreator = (upgradeRequest, upgradeResponse, callback) -> { |
||||||
|
if (subProtocol != null) { |
||||||
|
upgradeResponse.setAcceptedSubProtocol(subProtocol); |
||||||
|
} |
||||||
|
return adapter; |
||||||
|
}; |
||||||
|
|
||||||
|
Callback.Completable callback = new Callback.Completable(); |
||||||
|
Mono<Void> mono = Mono.fromFuture(callback); |
||||||
|
ServerWebSocketContainer container = getWebSocketServerContainer(jettyRequest); |
||||||
|
try { |
||||||
|
if (!container.upgrade(webSocketCreator, jettyRequest, jettyResponse, callback)) { |
||||||
|
throw new WebSocketException("request could not be upgraded to websocket"); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (WebSocketException ex) { |
||||||
|
callback.failed(ex); |
||||||
|
} |
||||||
|
|
||||||
|
return mono; |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
private ServerWebSocketContainer getWebSocketServerContainer(Request jettyRequest) { |
||||||
|
if (this.serverContainer == null) { |
||||||
|
Server server = jettyRequest.getConnectionMetaData().getConnector().getServer(); |
||||||
|
ServerWebSocketContainer container = ServerWebSocketContainer.get(server.getContext()); |
||||||
|
if (this.webSocketConfigurer != null) { |
||||||
|
this.webSocketConfigurer.accept(container); |
||||||
|
} |
||||||
|
this.serverContainer = container; |
||||||
|
} |
||||||
|
return this.serverContainer; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue