From 41ece612cf7f96256d042b4301928cecb102a304 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 8 Dec 2016 18:39:45 +0200 Subject: [PATCH 01/12] Generic AbstractRequest/ResponseBodyProcessor In preparation for use with WebSockets. Issue: SPR-14527 --- .../AbstractRequestBodyPublisher.java | 64 ++++++------ .../AbstractResponseBodyProcessor.java | 99 +++++++++---------- .../reactive/ServletServerHttpRequest.java | 2 +- .../reactive/ServletServerHttpResponse.java | 17 +++- .../reactive/UndertowServerHttpRequest.java | 2 +- .../reactive/UndertowServerHttpResponse.java | 21 +++- 6 files changed, 111 insertions(+), 94 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java index 79717c196a..cfc6fc6127 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java @@ -30,8 +30,6 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Operators; -import org.springframework.core.io.buffer.DataBuffer; - /** * Abstract base class for {@code Publisher} implementations that bridge between * event-listener APIs and Reactive Streams. Specifically, base class for the @@ -42,7 +40,7 @@ import org.springframework.core.io.buffer.DataBuffer; * @see ServletServerHttpRequest * @see UndertowHttpHandlerAdapter */ -abstract class AbstractRequestBodyPublisher implements Publisher { +public abstract class AbstractRequestBodyPublisher implements Publisher { protected final Log logger = LogFactory.getLog(getClass()); @@ -50,11 +48,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { private final AtomicLong demand = new AtomicLong(); - private Subscriber subscriber; + private Subscriber subscriber; @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber subscriber) { if (this.logger.isTraceEnabled()) { this.logger.trace(this.state + " subscribe: " + subscriber); } @@ -66,7 +64,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { * @see ReadListener#onDataAvailable() * @see org.xnio.ChannelListener#handleEvent(Channel) */ - protected final void onDataAvailable() { + public final void onDataAvailable() { if (this.logger.isTraceEnabled()) { this.logger.trace(this.state + " onDataAvailable"); } @@ -78,7 +76,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { * @see ReadListener#onAllDataRead() * @see org.xnio.ChannelListener#handleEvent(Channel) */ - protected final void onAllDataRead() { + public final void onAllDataRead() { if (this.logger.isTraceEnabled()) { this.logger.trace(this.state + " onAllDataRead"); } @@ -86,11 +84,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } /** - * Called by a listener interface to indicate that as error has occured. + * Called by a listener interface to indicate that as error has occurred. * @param t the error * @see ReadListener#onError(Throwable) */ - protected final void onError(Throwable t) { + public final void onError(Throwable t) { if (this.logger.isErrorEnabled()) { this.logger.error(this.state + " onError: " + t, t); } @@ -98,16 +96,16 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } /** - * Reads and publishes data buffers from the input. Continues till either there is no + * Reads and publishes data from the input. Continues till either there is no * more demand, or till there is no more data to be read. * @return {@code true} if there is more demand; {@code false} otherwise */ private boolean readAndPublish() throws IOException { while (hasDemand()) { - DataBuffer dataBuffer = read(); - if (dataBuffer != null) { + T data = read(); + if (data != null) { getAndSub(this.demand, 1L); - this.subscriber.onNext(dataBuffer); + this.subscriber.onNext(data); } else { return true; @@ -142,11 +140,11 @@ abstract class AbstractRequestBodyPublisher implements Publisher { protected abstract void checkOnDataAvailable(); /** - * Reads a data buffer from the input, if possible. Returns {@code null} if a buffer + * Reads a data from the input, if possible. Returns {@code null} if a data * could not be read. - * @return the data buffer that was read; or {@code null} + * @return the data that was read; or {@code null} */ - protected abstract DataBuffer read() throws IOException; + protected abstract T read() throws IOException; private boolean hasDemand() { return (this.demand.get() > 0); @@ -159,9 +157,9 @@ abstract class AbstractRequestBodyPublisher implements Publisher { private static final class RequestBodySubscription implements Subscription { - private final AbstractRequestBodyPublisher publisher; + private final AbstractRequestBodyPublisher publisher; - public RequestBodySubscription(AbstractRequestBodyPublisher publisher) { + public RequestBodySubscription(AbstractRequestBodyPublisher publisher) { this.publisher = publisher; } @@ -214,7 +212,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { */ UNSUBSCRIBED { @Override - void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { + void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { Objects.requireNonNull(subscriber); if (publisher.changeState(this, NO_DEMAND)) { Subscription subscription = new RequestBodySubscription(publisher); @@ -235,7 +233,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { */ NO_DEMAND { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractRequestBodyPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); if (publisher.changeState(this, DEMAND)) { @@ -253,14 +251,14 @@ abstract class AbstractRequestBodyPublisher implements Publisher { */ DEMAND { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractRequestBodyPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); } } @Override - void onDataAvailable(AbstractRequestBodyPublisher publisher) { + void onDataAvailable(AbstractRequestBodyPublisher publisher) { if (publisher.changeState(this, READING)) { try { boolean demandAvailable = publisher.readAndPublish(); @@ -281,7 +279,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { READING { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractRequestBodyPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); } @@ -293,40 +291,40 @@ abstract class AbstractRequestBodyPublisher implements Publisher { */ COMPLETED { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractRequestBodyPublisher publisher, long n) { // ignore } @Override - void cancel(AbstractRequestBodyPublisher publisher) { + void cancel(AbstractRequestBodyPublisher publisher) { // ignore } @Override - void onAllDataRead(AbstractRequestBodyPublisher publisher) { + void onAllDataRead(AbstractRequestBodyPublisher publisher) { // ignore } @Override - void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + void onError(AbstractRequestBodyPublisher publisher, Throwable t) { // ignore } }; - void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { + void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { throw new IllegalStateException(toString()); } - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractRequestBodyPublisher publisher, long n) { throw new IllegalStateException(toString()); } - void cancel(AbstractRequestBodyPublisher publisher) { + void cancel(AbstractRequestBodyPublisher publisher) { publisher.changeState(this, COMPLETED); } - void onDataAvailable(AbstractRequestBodyPublisher publisher) { + void onDataAvailable(AbstractRequestBodyPublisher publisher) { // ignore } - void onAllDataRead(AbstractRequestBodyPublisher publisher) { + void onAllDataRead(AbstractRequestBodyPublisher publisher) { if (publisher.changeState(this, COMPLETED)) { if (publisher.subscriber != null) { publisher.subscriber.onComplete(); @@ -334,7 +332,7 @@ abstract class AbstractRequestBodyPublisher implements Publisher { } } - void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + void onError(AbstractRequestBodyPublisher publisher, Throwable t) { if (publisher.changeState(this, COMPLETED)) { if (publisher.subscriber != null) { publisher.subscriber.onError(t); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java index f26456ff73..a272c62a36 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java @@ -29,8 +29,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.util.Assert; /** @@ -44,7 +42,7 @@ import org.springframework.util.Assert; * @see UndertowHttpHandlerAdapter * @see ServerHttpResponse#writeWith(Publisher) */ -abstract class AbstractResponseBodyProcessor implements Processor { +public abstract class AbstractResponseBodyProcessor implements Processor { protected final Log logger = LogFactory.getLog(getClass()); @@ -52,7 +50,7 @@ abstract class AbstractResponseBodyProcessor implements Processor state = new AtomicReference<>(State.UNSUBSCRIBED); - private volatile DataBuffer currentBuffer; + protected volatile T currentData; private volatile boolean subscriberCompleted; @@ -70,11 +68,11 @@ abstract class AbstractResponseBodyProcessor implements Processor void onSubscribe(AbstractResponseBodyProcessor processor, Subscription subscription) { Objects.requireNonNull(subscription, "Subscription cannot be null"); if (processor.changeState(this, REQUESTED)) { processor.subscription = subscription; @@ -209,7 +202,7 @@ abstract class AbstractResponseBodyProcessor implements Processor void onNext(AbstractResponseBodyProcessor processor, T data) { + if (processor.isDataEmpty(data)) { processor.subscription.request(1); } else { - processor.receiveBuffer(dataBuffer); + processor.receiveData(data); if (processor.changeState(this, RECEIVED)) { processor.writeIfPossible(); } @@ -230,16 +223,16 @@ abstract class AbstractResponseBodyProcessor implements Processor void onComplete(AbstractResponseBodyProcessor processor) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishComplete(); } } }, /** - * State that gets entered after a buffer has been + * State that gets entered after a data has been * {@linkplain Subscriber#onNext(Object) received}. Responds to - * {@code onWritePossible} by writing the current buffer and changes + * {@code onWritePossible} by writing the current data and changes * the state to {@link #WRITING}. If it can be written completely, * changes the state to either {@link #REQUESTED} if the subscription * has not been completed; or {@link #COMPLETED} if it has. If it cannot @@ -248,13 +241,13 @@ abstract class AbstractResponseBodyProcessor implements Processor void onWritePossible(AbstractResponseBodyProcessor processor) { if (processor.changeState(this, WRITING)) { - DataBuffer dataBuffer = processor.currentBuffer; + T data = processor.currentData; try { - boolean writeCompleted = processor.write(dataBuffer); + boolean writeCompleted = processor.write(data); if (writeCompleted) { - processor.releaseBuffer(); + processor.releaseData(); if (!processor.subscriberCompleted) { processor.changeState(WRITING, REQUESTED); processor.subscription.request(1); @@ -277,18 +270,18 @@ abstract class AbstractResponseBodyProcessor implements Processor void onComplete(AbstractResponseBodyProcessor processor) { processor.subscriberCompleted = true; } }, /** - * State that gets entered after a writing of the current buffer has been + * State that gets entered after a writing of the current data has been * {@code onWritePossible started}. */ WRITING { @Override - public void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractResponseBodyProcessor processor) { processor.subscriberCompleted = true; } }, @@ -298,40 +291,40 @@ abstract class AbstractResponseBodyProcessor implements Processor void onNext(AbstractResponseBodyProcessor processor, T data) { // ignore } @Override - public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { + public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { // ignore } @Override - public void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractResponseBodyProcessor processor) { // ignore } }; - public void onSubscribe(AbstractResponseBodyProcessor processor, Subscription subscription) { + public void onSubscribe(AbstractResponseBodyProcessor processor, Subscription subscription) { subscription.cancel(); } - public void onNext(AbstractResponseBodyProcessor processor, DataBuffer dataBuffer) { + public void onNext(AbstractResponseBodyProcessor processor, T data) { throw new IllegalStateException(toString()); } - public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { + public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishError(ex); } } - public void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractResponseBodyProcessor processor) { throw new IllegalStateException(toString()); } - public void onWritePossible(AbstractResponseBodyProcessor processor) { + public void onWritePossible(AbstractResponseBodyProcessor processor) { // ignore } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 22f012e594..58ee2f718b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -203,7 +203,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { private final RequestBodyPublisher.RequestBodyReadListener readListener = new RequestBodyPublisher.RequestBodyReadListener(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index f6151aa3a7..20cc5a6019 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -32,6 +32,7 @@ import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; @@ -183,7 +184,7 @@ public class ServletServerHttpResponse extends AbstractListenerServerHttpRespons } - private class ResponseBodyProcessor extends AbstractResponseBodyProcessor { + private class ResponseBodyProcessor extends AbstractResponseBodyProcessor { private final ServletOutputStream outputStream; @@ -199,6 +200,20 @@ public class ServletServerHttpResponse extends AbstractListenerServerHttpRespons return this.outputStream.isReady(); } + @Override + protected void releaseData() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentData); + } + DataBufferUtils.release(this.currentData); + this.currentData = null; + } + + @Override + protected boolean isDataEmpty(DataBuffer dataBuffer) { + return dataBuffer.readableByteCount() == 0; + } + @Override protected boolean write(DataBuffer dataBuffer) throws IOException { if (ServletServerHttpResponse.this.flushOnNext) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 2c5fad5218..54dd95af8e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -106,7 +106,7 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { return Flux.from(this.body); } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { private final ChannelListener readListener = new ReadListener(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 9aad48d13f..aaefc898eb 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -36,6 +36,7 @@ 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.DataBufferUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.ZeroCopyHttpOutputMessage; @@ -138,7 +139,7 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon } - private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { + private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { private final ChannelListener listener = new WriteListener(); @@ -187,17 +188,27 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon } @Override - protected void receiveBuffer(DataBuffer dataBuffer) { - super.receiveBuffer(dataBuffer); + protected void receiveData(DataBuffer dataBuffer) { + super.receiveData(dataBuffer); this.byteBuffer = dataBuffer.asByteBuffer(); } @Override - protected void releaseBuffer() { - super.releaseBuffer(); + protected void releaseData() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentData); + } + DataBufferUtils.release(this.currentData); + this.currentData = null; + this.byteBuffer = null; } + @Override + protected boolean isDataEmpty(DataBuffer dataBuffer) { + return dataBuffer.readableByteCount() == 0; + } + private class WriteListener implements ChannelListener { @Override From 46b39f4372859bc28e15cc0856c93e51e37631b1 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 8 Dec 2016 18:40:40 +0200 Subject: [PATCH 02/12] Initial reactive, WebSocket Tomcat support Issue: SPR-14527 --- build.gradle | 5 + .../TomcatWebSocketHandlerAdapter.java | 169 +++++++++++++ .../adapter/TomcatWebSocketSession.java | 234 ++++++++++++++++++ .../upgrade/ServerEndpointRegistration.java | 120 +++++++++ .../upgrade/TomcatRequestUpgradeStrategy.java | 94 +++++++ ...tractWebSocketHandlerIntegrationTests.java | 18 +- .../reactive/bootstrap/TomcatHttpServer.java | 10 + 7 files changed, 649 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/ServerEndpointRegistration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java diff --git a/build.gradle b/build.gradle index 6bf30460a1..ef2425a84b 100644 --- a/build.gradle +++ b/build.gradle @@ -824,6 +824,11 @@ project("spring-web-reactive") { } optional("io.reactivex:rxjava:${rxjavaVersion}") optional("io.reactivex:rxjava-reactive-streams:${rxjavaAdapterVersion}") + optional("javax.websocket:javax.websocket-api:${websocketVersion}") + optional("org.apache.tomcat:tomcat-websocket:${tomcatVersion}") { + exclude group: "org.apache.tomcat", module: "tomcat-websocket-api" + exclude group: "org.apache.tomcat", module: "tomcat-servlet-api" + } testCompile("io.projectreactor.addons:reactor-test:${reactorCoreVersion}") testCompile("javax.validation:validation-api:${beanvalVersion}") testCompile("org.hibernate:hibernate-validator:${hibval5Version}") diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java new file mode 100644 index 0000000000..e3066a5323 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.PongMessage; +import javax.websocket.Session; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; + +/** + * Tomcat {@code WebSocketHandler} implementation adapting and + * delegating to a Spring {@link WebSocketHandler}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class TomcatWebSocketHandlerAdapter extends Endpoint { + + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + + private final WebSocketHandler handler; + + private TomcatWebSocketSession wsSession; + + public TomcatWebSocketHandlerAdapter(WebSocketHandler handler) { + Assert.notNull("'handler' is required"); + this.handler = handler; + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + this.wsSession = new TomcatWebSocketSession(session); + + session.addMessageHandler(new MessageHandler.Whole() { + + @Override + public void onMessage(String message) { + while (true) { + if (wsSession.canWebSocketMessagePublisherAccept()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); + break; + } + } + } + + }); + session.addMessageHandler(new MessageHandler.Whole() { + + @Override + public void onMessage(ByteBuffer message) { + while (true) { + if (wsSession.canWebSocketMessagePublisherAccept()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); + break; + } + } + } + + }); + session.addMessageHandler(new MessageHandler.Whole() { + + @Override + public void onMessage(PongMessage message) { + while (true) { + if (wsSession.canWebSocketMessagePublisherAccept()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); + break; + } + } + } + + }); + + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); + this.handler.handle(this.wsSession).subscribe(resultSubscriber); + } + + @Override + public void onClose(Session session, CloseReason reason) { + if (this.wsSession != null) { + this.wsSession.handleClose(reason); + } + } + + @Override + public void onError(Session session, Throwable exception) { + if (this.wsSession != null) { + this.wsSession.handleError(exception); + } + } + + private WebSocketMessage toMessage(T message) { + if (message instanceof String) { + return WebSocketMessage.create(Type.TEXT, + bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + } + else if (message instanceof ByteBuffer) { + return WebSocketMessage.create(Type.BINARY, + bufferFactory.wrap((ByteBuffer) message)); + } + else if (message instanceof PongMessage) { + return WebSocketMessage.create(Type.PONG, + bufferFactory.wrap(((PongMessage) message).getApplicationData())); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message); + } + } + + private final class HandlerResultSubscriber implements Subscriber { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + // no op + } + + @Override + public void onError(Throwable ex) { + if (wsSession != null) { + wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + } + } + + @Override + public void onComplete() { + if (wsSession != null) { + wsSession.close(); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java new file mode 100644 index 0000000000..bc30d7c26b --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.websocket.CloseReason; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; +import javax.websocket.Session; +import javax.websocket.CloseReason.CloseCodes; + +import org.reactivestreams.Publisher; +import org.springframework.http.server.reactive.AbstractRequestBodyPublisher; +import org.springframework.http.server.reactive.AbstractResponseBodyProcessor; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Spring {@link WebSocketSession} adapter for Tomcat's + * {@link javax.websocket.Session}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class TomcatWebSocketSession extends WebSocketSessionSupport { + + private final AtomicBoolean sendCalled = new AtomicBoolean(); + + private final WebSocketMessagePublisher webSocketMessagePublisher = + new WebSocketMessagePublisher(); + + private final String id; + + private final URI uri; + + private volatile WebSocketMessageProcessor webSocketMessageProcessor; + + public TomcatWebSocketSession(Session session) { + super(session); + this.id = session.getId(); + this.uri = session.getRequestURI(); + } + + @Override + public String getId() { + return this.id; + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public Flux receive() { + return Flux.from(this.webSocketMessagePublisher); + } + + @Override + public Mono send(Publisher messages) { + if (this.sendCalled.compareAndSet(false, true)) { + this.webSocketMessageProcessor = new WebSocketMessageProcessor(); + return Mono.from(subscriber -> { + messages.subscribe(this.webSocketMessageProcessor); + this.webSocketMessageProcessor.subscribe(subscriber); + }); + } + else { + return Mono.error(new IllegalStateException("send() has already been called")); + } + } + + @Override + protected Mono closeInternal(CloseStatus status) { + try { + getDelegate().close(new CloseReason(CloseCodes.getCloseCode(status.getCode()), status.getReason())); + } + catch (IOException e) { + return Mono.error(e); + } + return Mono.empty(); + } + + boolean canWebSocketMessagePublisherAccept() { + return this.webSocketMessagePublisher.canAccept(); + } + + /** Handle a message callback from the Servlet container */ + void handleMessage(Type type, WebSocketMessage message) { + this.webSocketMessagePublisher.processWebSocketMessage(message); + } + + /** Handle a error callback from the Servlet container */ + void handleError(Throwable ex) { + this.webSocketMessagePublisher.onError(ex); + if (this.webSocketMessageProcessor != null) { + this.webSocketMessageProcessor.cancel(); + this.webSocketMessageProcessor.onError(ex); + } + } + + /** Handle a complete callback from the Servlet container */ + void handleClose(CloseReason reason) { + this.webSocketMessagePublisher.onAllDataRead(); + if (this.webSocketMessageProcessor != null) { + this.webSocketMessageProcessor.cancel(); + this.webSocketMessageProcessor.onComplete(); + } + } + + private static final class WebSocketMessagePublisher extends AbstractRequestBodyPublisher { + private volatile WebSocketMessage webSocketMessage; + + @Override + protected void checkOnDataAvailable() { + if (this.webSocketMessage != null) { + onDataAvailable(); + } + } + + @Override + protected WebSocketMessage read() throws IOException { + if (this.webSocketMessage != null) { + WebSocketMessage result = this.webSocketMessage; + this.webSocketMessage = null; + return result; + } + + return null; + } + + void processWebSocketMessage(WebSocketMessage webSocketMessage) { + this.webSocketMessage = webSocketMessage; + onDataAvailable(); + } + + boolean canAccept() { + return this.webSocketMessage == null; + } + } + + private final class WebSocketMessageProcessor extends AbstractResponseBodyProcessor { + private volatile boolean isReady = true; + + @Override + protected boolean write(WebSocketMessage message) throws IOException { + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + this.isReady = false; + getDelegate().getAsyncRemote().sendText( + new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), + new WebSocketMessageSendHandler()); + } + else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { + this.isReady = false; + getDelegate().getAsyncRemote().sendBinary(message.getPayload().asByteBuffer(), + new WebSocketMessageSendHandler()); + } + else if (WebSocketMessage.Type.PING.equals(message.getType())) { + getDelegate().getAsyncRemote().sendPing(message.getPayload().asByteBuffer()); + } + else if (WebSocketMessage.Type.PONG.equals(message.getType())) { + getDelegate().getAsyncRemote().sendPong(message.getPayload().asByteBuffer()); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + return true; + } + + @Override + protected void releaseData() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentData); + } + this.currentData = null; + } + + @Override + protected boolean isDataEmpty(WebSocketMessage data) { + return data.getPayload().readableByteCount() == 0; + } + + @Override + protected boolean isWritePossible() { + if (this.isReady && this.currentData != null) { + return true; + } + else { + return false; + } + } + + private final class WebSocketMessageSendHandler implements SendHandler { + + @Override + public void onResult(SendResult result) { + if (result.isOK()) { + isReady = true; + webSocketMessageProcessor.onWritePossible(); + } + else { + webSocketMessageProcessor.cancel(); + webSocketMessageProcessor.onError(result.getException()); + } + } + + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/ServerEndpointRegistration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/ServerEndpointRegistration.java new file mode 100644 index 0000000000..63c5868540 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/ServerEndpointRegistration.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.websocket.Decoder; +import javax.websocket.Encoder; +import javax.websocket.Endpoint; +import javax.websocket.Extension; +import javax.websocket.server.ServerEndpointConfig; + +import org.springframework.util.Assert; + +/** + * An implementation of {@link javax.websocket.server.ServerEndpointConfig} for use in + * Spring applications. + * + *

Class constructor accept a singleton {@link javax.websocket.Endpoint} instance. + * + *

This class also extends + * {@link javax.websocket.server.ServerEndpointConfig.Configurator} to make it easier to + * override methods for customizing the handshake process. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class ServerEndpointRegistration extends ServerEndpointConfig.Configurator + implements ServerEndpointConfig { + + private final String path; + + private final Endpoint endpoint; + + /** + * Create a new {@link ServerEndpointRegistration} instance from an + * {@code javax.websocket.Endpoint} instance. + * @param path the endpoint path + * @param endpoint the endpoint instance + */ + public ServerEndpointRegistration(String path, Endpoint endpoint) { + Assert.hasText(path, "path must not be empty"); + Assert.notNull(endpoint, "endpoint must not be null"); + this.path = path; + this.endpoint = endpoint; + } + + @Override + public List> getEncoders() { + return new ArrayList<>(); + } + + @Override + public List> getDecoders() { + return new ArrayList<>(); + } + + @Override + public Map getUserProperties() { + return new HashMap<>(); + } + + @Override + public Class getEndpointClass() { + return this.endpoint.getClass(); + } + + public Endpoint getEndpoint() { + return this.endpoint; + } + + @Override + public String getPath() { + return this.path; + } + + @Override + public List getSubprotocols() { + return new ArrayList<>(); + } + + @Override + public List getExtensions() { + return new ArrayList<>(); + } + + @Override + public Configurator getConfigurator() { + return this; + } + + @SuppressWarnings("unchecked") + @Override + public T getEndpointInstance(Class endpointClass) + throws InstantiationException { + return (T) getEndpoint(); + } + + @Override + public String toString() { + return "ServerEndpointRegistration for path '" + getPath() + "': " + getEndpointClass(); + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java new file mode 100644 index 0000000000..899973892c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.io.IOException; +import java.util.Collections; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.websocket.server.WsServerContainer; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServletServerHttpRequest; +import org.springframework.http.server.reactive.ServletServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.TomcatWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * A {@link RequestUpgradeStrategy} for use with Tomcat. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class TomcatRequestUpgradeStrategy implements RequestUpgradeStrategy { + + private static final String SERVER_CONTAINER_ATTR = "javax.websocket.server.ServerContainer"; + + + @Override + public Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler){ + + TomcatWebSocketHandlerAdapter endpoint = + new TomcatWebSocketHandlerAdapter(webSocketHandler); + + HttpServletRequest servletRequest = getHttpServletRequest(exchange.getRequest()); + HttpServletResponse servletResponse = getHttpServletResponse(exchange.getResponse()); + + Map pathParams = Collections. emptyMap(); + + ServerEndpointRegistration sec = + new ServerEndpointRegistration(servletRequest.getRequestURI(), endpoint); + try { + getContainer(servletRequest).doUpgrade(servletRequest, servletResponse, + sec, pathParams); + } + catch (ServletException | IOException e) { + return Mono.error(e); + } + + return Mono.empty(); + } + + private WsServerContainer getContainer(HttpServletRequest request) { + ServletContext servletContext = request.getServletContext(); + Object container = servletContext.getAttribute(SERVER_CONTAINER_ATTR); + Assert.notNull(container, "No '" + SERVER_CONTAINER_ATTR + "' ServletContext attribute. " + + "Are you running in a Servlet container that supports JSR-356?"); + Assert.isTrue(container instanceof WsServerContainer); + return (WsServerContainer) container; + } + + private final HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { + Assert.isTrue(request instanceof ServletServerHttpRequest); + return ((ServletServerHttpRequest) request).getServletRequest(); + } + + private final HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { + Assert.isTrue(response instanceof ServletServerHttpResponse); + return ((ServletServerHttpResponse) response).getServletResponse(); + } +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java index 2489211ba0..de1d4d9b2a 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java @@ -15,6 +15,9 @@ */ package org.springframework.web.reactive.socket.server; +import java.io.File; + +import org.apache.tomcat.websocket.server.WsContextListener; import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; @@ -29,12 +32,14 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.bootstrap.HttpServer; import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; +import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.util.SocketUtils; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.RxNettyRequestUpgradeStrategy; +import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy; /** * Base class for WebSocket integration tests involving a server-side @@ -59,9 +64,11 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { @Parameters public static Object[][] arguments() { + File base = new File(System.getProperty("java.io.tmpdir")); return new Object[][] { {new ReactorHttpServer(), ReactorNettyConfig.class}, - {new RxNettyHttpServer(), RxNettyConfig.class} + {new RxNettyHttpServer(), RxNettyConfig.class}, + {new TomcatHttpServer(base.getAbsolutePath(), WsContextListener.class), TomcatConfig.class} }; } @@ -134,4 +141,13 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { } } + @Configuration + static class TomcatConfig extends AbstractHandlerAdapterConfig { + + @Override + protected RequestUpgradeStrategy getUpgradeStrategy() { + return new TomcatRequestUpgradeStrategy(); + } + } + } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java index 0c854e63bd..62337269f5 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java @@ -37,6 +37,8 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I private String baseDir; + private Class wsListener; + public TomcatHttpServer() { } @@ -45,6 +47,11 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I this.baseDir = baseDir; } + public TomcatHttpServer(String baseDir, Class wsListener) { + this.baseDir = baseDir; + this.wsListener = wsListener; + } + @Override public void afterPropertiesSet() throws Exception { @@ -61,6 +68,9 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I Context rootContext = tomcatServer.addContext("", base.getAbsolutePath()); Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet); rootContext.addServletMappingDecoded("/", "httpHandlerServlet"); + if (wsListener != null) { + rootContext.addApplicationListener(wsListener.getName()); + } } private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() { From 80040ef43b260960e65bdadcc48a8eed663c45a8 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 8 Dec 2016 18:41:08 +0200 Subject: [PATCH 03/12] Initial reactive, WebSocket Undertow support Issue: SPR-14527 --- build.gradle | 3 + ...stractListenerWebSocketSessionSupport.java | 195 ++++++++++++++++++ .../TomcatWebSocketHandlerAdapter.java | 3 +- .../adapter/TomcatWebSocketSession.java | 194 +++-------------- .../UndertowWebSocketHandlerAdapter.java | 156 ++++++++++++++ .../adapter/UndertowWebSocketSession.java | 111 ++++++++++ .../UndertowRequestUpgradeStrategy.java | 63 ++++++ ...tractWebSocketHandlerIntegrationTests.java | 14 +- 8 files changed, 575 insertions(+), 164 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java diff --git a/build.gradle b/build.gradle index ef2425a84b..7c7f576b7f 100644 --- a/build.gradle +++ b/build.gradle @@ -829,6 +829,9 @@ project("spring-web-reactive") { exclude group: "org.apache.tomcat", module: "tomcat-websocket-api" exclude group: "org.apache.tomcat", module: "tomcat-servlet-api" } + optional("io.undertow:undertow-websockets-jsr:${undertowVersion}") { + exclude group: "org.jboss.spec.javax.websocket", module: "jboss-websocket-api_1.1_spec" + } testCompile("io.projectreactor.addons:reactor-test:${reactorCoreVersion}") testCompile("javax.validation:validation-api:${beanvalVersion}") testCompile("org.hibernate:hibernate-validator:${hibval5Version}") diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java new file mode 100644 index 0000000000..44fbe4bb84 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.io.IOException; +import java.net.URI; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Publisher; +import org.springframework.http.server.reactive.AbstractRequestBodyPublisher; +import org.springframework.http.server.reactive.AbstractResponseBodyProcessor; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Base class for Listener-based {@link WebSocketSession} adapters. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public abstract class AbstractListenerWebSocketSessionSupport extends WebSocketSessionSupport { + + private final AtomicBoolean sendCalled = new AtomicBoolean(); + + private final String id; + + private final URI uri; + + protected final WebSocketMessagePublisher webSocketMessagePublisher = + new WebSocketMessagePublisher(); + + protected volatile WebSocketMessageProcessor webSocketMessageProcessor; + + public AbstractListenerWebSocketSessionSupport(T delegate, String id, URI uri) { + super(delegate); + Assert.notNull(id, "'id' is required."); + Assert.notNull(uri, "'uri' is required."); + this.id = id; + this.uri = uri; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public Flux receive() { + return Flux.from(this.webSocketMessagePublisher); + } + + @Override + public Mono send(Publisher messages) { + if (this.sendCalled.compareAndSet(false, true)) { + this.webSocketMessageProcessor = new WebSocketMessageProcessor(); + return Mono.from(subscriber -> { + messages.subscribe(this.webSocketMessageProcessor); + this.webSocketMessageProcessor.subscribe(subscriber); + }); + } + else { + return Mono.error(new IllegalStateException("send() has already been called")); + } + } + + protected void resumeReceives() { + // no-op + } + + protected void suspendReceives() { + // no-op + } + + protected abstract boolean writeInternal(WebSocketMessage message) throws IOException; + + /** Handle a message callback from the Servlet container */ + void handleMessage(Type type, WebSocketMessage message) { + this.webSocketMessagePublisher.processWebSocketMessage(message); + } + + /** Handle a error callback from the Servlet container */ + void handleError(Throwable ex) { + this.webSocketMessagePublisher.onError(ex); + if (this.webSocketMessageProcessor != null) { + this.webSocketMessageProcessor.cancel(); + this.webSocketMessageProcessor.onError(ex); + } + } + + /** Handle a complete callback from the Servlet container */ + void handleClose(CloseStatus reason) { + this.webSocketMessagePublisher.onAllDataRead(); + if (this.webSocketMessageProcessor != null) { + this.webSocketMessageProcessor.cancel(); + this.webSocketMessageProcessor.onComplete(); + } + } + + final class WebSocketMessagePublisher extends AbstractRequestBodyPublisher { + private volatile WebSocketMessage webSocketMessage; + + @Override + protected void checkOnDataAvailable() { + if (this.webSocketMessage != null) { + onDataAvailable(); + } + } + + @Override + protected WebSocketMessage read() throws IOException { + if (this.webSocketMessage != null) { + WebSocketMessage result = this.webSocketMessage; + this.webSocketMessage = null; + resumeReceives(); + return result; + } + + return null; + } + + void processWebSocketMessage(WebSocketMessage webSocketMessage) { + this.webSocketMessage = webSocketMessage; + suspendReceives(); + onDataAvailable(); + } + + boolean canAccept() { + return this.webSocketMessage == null; + } + } + + final class WebSocketMessageProcessor extends AbstractResponseBodyProcessor { + private volatile boolean isReady = true; + + @Override + protected boolean write(WebSocketMessage message) throws IOException { + return writeInternal(message); + } + + @Override + protected void releaseData() { + if (logger.isTraceEnabled()) { + logger.trace("releaseBuffer: " + this.currentData); + } + this.currentData = null; + } + + @Override + protected boolean isDataEmpty(WebSocketMessage data) { + return data.getPayload().readableByteCount() == 0; + } + + @Override + protected boolean isWritePossible() { + if (this.isReady && this.currentData != null) { + return true; + } + else { + return false; + } + } + + void setReady(boolean ready) { + this.isReady = ready; + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java index e3066a5323..12fa2980e8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -110,7 +110,8 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onClose(Session session, CloseReason reason) { if (this.wsSession != null) { - this.wsSession.handleClose(reason); + this.wsSession.handleClose( + new CloseStatus(reason.getCloseCode().getCode(), reason.getReasonPhrase())); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java index bc30d7c26b..3e9ab0258a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java @@ -17,9 +17,7 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; -import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicBoolean; import javax.websocket.CloseReason; import javax.websocket.SendHandler; @@ -27,15 +25,10 @@ import javax.websocket.SendResult; import javax.websocket.Session; import javax.websocket.CloseReason.CloseCodes; -import org.reactivestreams.Publisher; -import org.springframework.http.server.reactive.AbstractRequestBodyPublisher; -import org.springframework.http.server.reactive.AbstractResponseBodyProcessor; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketMessage; import org.springframework.web.reactive.socket.WebSocketSession; -import org.springframework.web.reactive.socket.WebSocketMessage.Type; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -45,58 +38,17 @@ import reactor.core.publisher.Mono; * @author Violeta Georgieva * @since 5.0 */ -public class TomcatWebSocketSession extends WebSocketSessionSupport { - - private final AtomicBoolean sendCalled = new AtomicBoolean(); - - private final WebSocketMessagePublisher webSocketMessagePublisher = - new WebSocketMessagePublisher(); - - private final String id; - - private final URI uri; - - private volatile WebSocketMessageProcessor webSocketMessageProcessor; +public class TomcatWebSocketSession extends AbstractListenerWebSocketSessionSupport { public TomcatWebSocketSession(Session session) { - super(session); - this.id = session.getId(); - this.uri = session.getRequestURI(); - } - - @Override - public String getId() { - return this.id; - } - - @Override - public URI getUri() { - return this.uri; - } - - @Override - public Flux receive() { - return Flux.from(this.webSocketMessagePublisher); - } - - @Override - public Mono send(Publisher messages) { - if (this.sendCalled.compareAndSet(false, true)) { - this.webSocketMessageProcessor = new WebSocketMessageProcessor(); - return Mono.from(subscriber -> { - messages.subscribe(this.webSocketMessageProcessor); - this.webSocketMessageProcessor.subscribe(subscriber); - }); - } - else { - return Mono.error(new IllegalStateException("send() has already been called")); - } + super(session, session.getId(), session.getRequestURI()); } @Override protected Mono closeInternal(CloseStatus status) { try { - getDelegate().close(new CloseReason(CloseCodes.getCloseCode(status.getCode()), status.getReason())); + getDelegate().close( + new CloseReason(CloseCodes.getCloseCode(status.getCode()), status.getReason())); } catch (IOException e) { return Mono.error(e); @@ -108,125 +60,43 @@ public class TomcatWebSocketSession extends WebSocketSessionSupport { return this.webSocketMessagePublisher.canAccept(); } - /** Handle a message callback from the Servlet container */ - void handleMessage(Type type, WebSocketMessage message) { - this.webSocketMessagePublisher.processWebSocketMessage(message); - } - - /** Handle a error callback from the Servlet container */ - void handleError(Throwable ex) { - this.webSocketMessagePublisher.onError(ex); - if (this.webSocketMessageProcessor != null) { - this.webSocketMessageProcessor.cancel(); - this.webSocketMessageProcessor.onError(ex); + @Override + protected boolean writeInternal(WebSocketMessage message) throws IOException { + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + getDelegate().getAsyncRemote().sendText( + new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), + new WebSocketMessageSendHandler()); } - } - - /** Handle a complete callback from the Servlet container */ - void handleClose(CloseReason reason) { - this.webSocketMessagePublisher.onAllDataRead(); - if (this.webSocketMessageProcessor != null) { - this.webSocketMessageProcessor.cancel(); - this.webSocketMessageProcessor.onComplete(); + else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + getDelegate().getAsyncRemote().sendBinary(message.getPayload().asByteBuffer(), + new WebSocketMessageSendHandler()); } + else if (WebSocketMessage.Type.PING.equals(message.getType())) { + getDelegate().getAsyncRemote().sendPing(message.getPayload().asByteBuffer()); + } + else if (WebSocketMessage.Type.PONG.equals(message.getType())) { + getDelegate().getAsyncRemote().sendPong(message.getPayload().asByteBuffer()); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + return true; } - private static final class WebSocketMessagePublisher extends AbstractRequestBodyPublisher { - private volatile WebSocketMessage webSocketMessage; + private final class WebSocketMessageSendHandler implements SendHandler { @Override - protected void checkOnDataAvailable() { - if (this.webSocketMessage != null) { - onDataAvailable(); - } - } - - @Override - protected WebSocketMessage read() throws IOException { - if (this.webSocketMessage != null) { - WebSocketMessage result = this.webSocketMessage; - this.webSocketMessage = null; - return result; - } - - return null; - } - - void processWebSocketMessage(WebSocketMessage webSocketMessage) { - this.webSocketMessage = webSocketMessage; - onDataAvailable(); - } - - boolean canAccept() { - return this.webSocketMessage == null; - } - } - - private final class WebSocketMessageProcessor extends AbstractResponseBodyProcessor { - private volatile boolean isReady = true; - - @Override - protected boolean write(WebSocketMessage message) throws IOException { - if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - this.isReady = false; - getDelegate().getAsyncRemote().sendText( - new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), - new WebSocketMessageSendHandler()); - } - else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - this.isReady = false; - getDelegate().getAsyncRemote().sendBinary(message.getPayload().asByteBuffer(), - new WebSocketMessageSendHandler()); - } - else if (WebSocketMessage.Type.PING.equals(message.getType())) { - getDelegate().getAsyncRemote().sendPing(message.getPayload().asByteBuffer()); - } - else if (WebSocketMessage.Type.PONG.equals(message.getType())) { - getDelegate().getAsyncRemote().sendPong(message.getPayload().asByteBuffer()); + public void onResult(SendResult result) { + if (result.isOK()) { + webSocketMessageProcessor.setReady(true); + webSocketMessageProcessor.onWritePossible(); } else { - throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + webSocketMessageProcessor.cancel(); + webSocketMessageProcessor.onError(result.getException()); } - return true; - } - - @Override - protected void releaseData() { - if (logger.isTraceEnabled()) { - logger.trace("releaseBuffer: " + this.currentData); - } - this.currentData = null; - } - - @Override - protected boolean isDataEmpty(WebSocketMessage data) { - return data.getPayload().readableByteCount() == 0; - } - - @Override - protected boolean isWritePossible() { - if (this.isReady && this.currentData != null) { - return true; - } - else { - return false; - } - } - - private final class WebSocketMessageSendHandler implements SendHandler { - - @Override - public void onResult(SendResult result) { - if (result.isOK()) { - isReady = true; - webSocketMessageProcessor.onWritePossible(); - } - else { - webSocketMessageProcessor.cancel(); - webSocketMessageProcessor.onError(result.getException()); - } - } - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java new file mode 100644 index 0000000000..ec4ea0c155 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; + +import io.undertow.websockets.WebSocketConnectionCallback; +import io.undertow.websockets.core.AbstractReceiveListener; +import io.undertow.websockets.core.BufferedBinaryMessage; +import io.undertow.websockets.core.BufferedTextMessage; +import io.undertow.websockets.core.CloseMessage; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.spi.WebSocketHttpExchange; + +/** + * Undertow {@code WebSocketHandler} implementation adapting and + * delegating to a Spring {@link WebSocketHandler}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class UndertowWebSocketHandlerAdapter implements WebSocketConnectionCallback { + + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + + private final WebSocketHandler handler; + + private UndertowWebSocketSession wsSession; + + public UndertowWebSocketHandlerAdapter(WebSocketHandler handler) { + Assert.notNull("'handler' is required"); + this.handler = handler; + } + + @Override + public void onConnect(WebSocketHttpExchange exchange, WebSocketChannel channel) { + try { + this.wsSession = new UndertowWebSocketSession(channel); + } + catch (URISyntaxException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + channel.getReceiveSetter().set(new ReceiveListener()); + channel.resumeReceives(); + + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); + this.handler.handle(this.wsSession).subscribe(resultSubscriber); + } + + private final class ReceiveListener extends AbstractReceiveListener { + + @Override + protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) { + wsSession.handleMessage(Type.TEXT, toMessage(Type.TEXT, message.getData())); + } + + @Override + protected void onFullBinaryMessage(WebSocketChannel channel, + BufferedBinaryMessage message) throws IOException { + wsSession.handleMessage(Type.BINARY, toMessage(Type.BINARY, message.getData().getResource())); + message.getData().free(); + } + + @Override + protected void onFullPongMessage(WebSocketChannel channel, + BufferedBinaryMessage message) throws IOException { + wsSession.handleMessage(Type.PONG, toMessage(Type.PONG, message.getData().getResource())); + message.getData().free(); + } + + @Override + protected void onFullCloseMessage(WebSocketChannel channel, + BufferedBinaryMessage message) throws IOException { + CloseMessage closeMessage = new CloseMessage(message.getData().getResource()); + wsSession.handleClose(new CloseStatus(closeMessage.getCode(), closeMessage.getReason())); + message.getData().free(); + } + + @Override + protected void onError(WebSocketChannel channel, Throwable error) { + wsSession.handleError(error); + } + + private WebSocketMessage toMessage(Type type, T message) { + if (Type.TEXT.equals(type)) { + return WebSocketMessage.create(Type.TEXT, + bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + } + else if (Type.BINARY.equals(type)) { + return WebSocketMessage.create(Type.BINARY, + bufferFactory.allocateBuffer().write((ByteBuffer[]) message)); + } + else if (Type.PONG.equals(type)) { + return WebSocketMessage.create(Type.PONG, + bufferFactory.allocateBuffer().write((ByteBuffer[]) message)); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message); + } + } + + } + + private final class HandlerResultSubscriber implements Subscriber { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + // no op + } + + @Override + public void onError(Throwable ex) { + wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + } + + @Override + public void onComplete() { + wsSession.close(); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java new file mode 100644 index 0000000000..bacad80f64 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; + +import org.springframework.util.ObjectUtils; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; + +import io.undertow.websockets.core.CloseMessage; +import io.undertow.websockets.core.WebSocketCallback; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSockets; +import reactor.core.publisher.Mono; + +/** + * Spring {@link WebSocketSession} adapter for Undertow's + * {@link io.undertow.websockets.core.WebSocketChannel}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class UndertowWebSocketSession extends AbstractListenerWebSocketSessionSupport { + + public UndertowWebSocketSession(WebSocketChannel channel) throws URISyntaxException { + super(channel, ObjectUtils.getIdentityHexString(channel), new URI(channel.getUrl())); + } + + @Override + protected Mono closeInternal(CloseStatus status) { + CloseMessage cm = new CloseMessage(status.getCode(), status.getReason()); + if (!getDelegate().isCloseFrameSent()) { + WebSockets.sendClose(cm, getDelegate(), null); + } + return Mono.empty(); + } + + protected void resumeReceives() { + getDelegate().resumeReceives(); + } + + protected void suspendReceives() { + getDelegate().suspendReceives(); + } + + @Override + protected boolean writeInternal(WebSocketMessage message) throws IOException { + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + WebSockets.sendText( + new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), + getDelegate(), new WebSocketMessageSendHandler()); + } + else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + WebSockets.sendBinary(message.getPayload().asByteBuffer(), + getDelegate(), new WebSocketMessageSendHandler()); + } + else if (WebSocketMessage.Type.PING.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + WebSockets.sendPing(message.getPayload().asByteBuffer(), + getDelegate(), new WebSocketMessageSendHandler()); + } + else if (WebSocketMessage.Type.PONG.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + WebSockets.sendPong(message.getPayload().asByteBuffer(), + getDelegate(), new WebSocketMessageSendHandler()); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + return true; + } + + private final class WebSocketMessageSendHandler implements WebSocketCallback { + + @Override + public void complete(WebSocketChannel channel, Void context) { + webSocketMessageProcessor.setReady(true); + webSocketMessageProcessor.onWritePossible(); + } + + @Override + public void onError(WebSocketChannel channel, Void context, + Throwable throwable) { + webSocketMessageProcessor.cancel(); + webSocketMessageProcessor.onError(throwable); + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java new file mode 100644 index 0000000000..7cc22ddda7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.UndertowServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.UndertowWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; +import org.springframework.web.server.ServerWebExchange; + +import io.undertow.server.HttpServerExchange; +import io.undertow.websockets.WebSocketProtocolHandshakeHandler; +import reactor.core.publisher.Mono; + +/** +* A {@link RequestUpgradeStrategy} for use with Undertow. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class UndertowRequestUpgradeStrategy implements RequestUpgradeStrategy { + + @Override + public Mono upgrade(ServerWebExchange exchange, + WebSocketHandler webSocketHandler) { + + UndertowWebSocketHandlerAdapter callback = + new UndertowWebSocketHandlerAdapter(webSocketHandler); + + WebSocketProtocolHandshakeHandler handler = + new WebSocketProtocolHandshakeHandler(callback); + try { + handler.handleRequest(getUndertowExchange(exchange.getRequest())); + } + catch (Exception e) { + return Mono.error(e); + } + + return Mono.empty(); + } + + private final HttpServerExchange getUndertowExchange(ServerHttpRequest request) { + Assert.isTrue(request instanceof UndertowServerHttpRequest); + return ((UndertowServerHttpRequest) request).getUndertowExchange(); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java index de1d4d9b2a..5d494253cb 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.http.server.reactive.bootstrap.HttpServer; import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer; +import org.springframework.http.server.reactive.bootstrap.UndertowHttpServer; import org.springframework.util.SocketUtils; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; @@ -40,6 +41,7 @@ import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAd import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.RxNettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy; +import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; /** * Base class for WebSocket integration tests involving a server-side @@ -68,7 +70,8 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { return new Object[][] { {new ReactorHttpServer(), ReactorNettyConfig.class}, {new RxNettyHttpServer(), RxNettyConfig.class}, - {new TomcatHttpServer(base.getAbsolutePath(), WsContextListener.class), TomcatConfig.class} + {new TomcatHttpServer(base.getAbsolutePath(), WsContextListener.class), TomcatConfig.class}, + {new UndertowHttpServer(), UndertowConfig.class} }; } @@ -150,4 +153,13 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { } } + @Configuration + static class UndertowConfig extends AbstractHandlerAdapterConfig { + + @Override + protected RequestUpgradeStrategy getUpgradeStrategy() { + return new UndertowRequestUpgradeStrategy(); + } + } + } From a2053a516e7d28e0bc924fd29a6822a2a9d70675 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Thu, 8 Dec 2016 19:47:41 +0200 Subject: [PATCH 04/12] Initial reactive, WebSocket Jetty support Issue: SPR-14527 --- build.gradle | 3 + .../adapter/JettyWebSocketHandlerAdapter.java | 158 ++++++++++++++++++ .../socket/adapter/JettyWebSocketSession.java | 92 ++++++++++ .../upgrade/JettyRequestUpgradeStrategy.java | 146 ++++++++++++++++ ...tractWebSocketHandlerIntegrationTests.java | 14 +- 5 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java diff --git a/build.gradle b/build.gradle index 7c7f576b7f..8ace9a7022 100644 --- a/build.gradle +++ b/build.gradle @@ -829,6 +829,9 @@ project("spring-web-reactive") { exclude group: "org.apache.tomcat", module: "tomcat-websocket-api" exclude group: "org.apache.tomcat", module: "tomcat-servlet-api" } + optional("org.eclipse.jetty.websocket:websocket-server:${jettyVersion}") { + exclude group: "javax.servlet", module: "javax.servlet" + } optional("io.undertow:undertow-websockets-jsr:${undertowVersion}") { exclude group: "org.jboss.spec.javax.websocket", module: "jboss-websocket-api_1.1_spec" } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java new file mode 100644 index 0000000000..815bd2da79 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +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.WebSocket; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.common.OpCode; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; + +/** + * Jetty {@code WebSocketHandler} implementation adapting and + * delegating to a Spring {@link WebSocketHandler}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +@WebSocket +public class JettyWebSocketHandlerAdapter { + + private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); + + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + + private final WebSocketHandler handler; + + private JettyWebSocketSession wsSession; + + public JettyWebSocketHandlerAdapter(WebSocketHandler handler) { + Assert.notNull("'handler' is required"); + this.handler = handler; + } + + @OnWebSocketConnect + public void onWebSocketConnect(Session session) { + this.wsSession = new JettyWebSocketSession(session); + + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); + this.handler.handle(this.wsSession).subscribe(resultSubscriber); + } + + @OnWebSocketMessage + public void onWebSocketText(String message) { + if (this.wsSession != null) { + WebSocketMessage wsMessage = toMessage(Type.TEXT, message); + this.wsSession.handleMessage(wsMessage.getType(), wsMessage); + } + } + + @OnWebSocketMessage + public void onWebSocketBinary(byte[] message, int offset, int length) { + if (this.wsSession != null) { + WebSocketMessage wsMessage = toMessage(Type.BINARY, ByteBuffer.wrap(message, offset, length)); + wsSession.handleMessage(wsMessage.getType(), wsMessage); + } + } + + @OnWebSocketFrame + public void onWebSocketFrame(Frame frame) { + if (this.wsSession != null) { + if (OpCode.PONG == frame.getOpCode()) { + ByteBuffer message = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; + WebSocketMessage wsMessage = toMessage(Type.PONG, message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); + } + } + } + + @OnWebSocketClose + public void onWebSocketClose(int statusCode, String reason) { + if (this.wsSession != null) { + this.wsSession.handleClose(new CloseStatus(statusCode, reason)); + } + } + + @OnWebSocketError + public void onWebSocketError(Throwable cause) { + if (this.wsSession != null) { + this.wsSession.handleError(cause); + } + } + + private WebSocketMessage toMessage(Type type, T message) { + if (Type.TEXT.equals(type)) { + return WebSocketMessage.create(Type.TEXT, + bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + } + else if (Type.BINARY.equals(type)) { + return WebSocketMessage.create(Type.BINARY, + bufferFactory.wrap((ByteBuffer) message)); + } + else if (Type.PONG.equals(type)) { + return WebSocketMessage.create(Type.PONG, + bufferFactory.wrap((ByteBuffer) message)); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message); + } + } + + private final class HandlerResultSubscriber implements Subscriber { + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + // no op + } + + @Override + public void onError(Throwable ex) { + if (wsSession != null) { + wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + } + } + + @Override + public void onComplete() { + if (wsSession != null) { + wsSession.close(); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java new file mode 100644 index 0000000000..0e7e330ef7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.adapter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.springframework.util.ObjectUtils; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; + +import reactor.core.publisher.Mono; + +/** + * Spring {@link WebSocketSession} adapter for Jetty's + * {@link org.eclipse.jetty.websocket.api.Session}. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class JettyWebSocketSession extends AbstractListenerWebSocketSessionSupport { + + public JettyWebSocketSession(Session session) { + super(session, ObjectUtils.getIdentityHexString(session), + session.getUpgradeRequest().getRequestURI()); + } + + @Override + protected Mono closeInternal(CloseStatus status) { + getDelegate().close(status.getCode(), status.getReason()); + return Mono.empty(); + } + + @Override + protected boolean writeInternal(WebSocketMessage message) throws IOException { + if (WebSocketMessage.Type.TEXT.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + getDelegate().getRemote().sendString( + new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), + new WebSocketMessageWriteCallback()); + } + else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { + this.webSocketMessageProcessor.setReady(false); + getDelegate().getRemote().sendBytes(message.getPayload().asByteBuffer(), + new WebSocketMessageWriteCallback()); + } + else if (WebSocketMessage.Type.PING.equals(message.getType())) { + getDelegate().getRemote().sendPing(message.getPayload().asByteBuffer()); + } + else if (WebSocketMessage.Type.PONG.equals(message.getType())) { + getDelegate().getRemote().sendPong(message.getPayload().asByteBuffer()); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message.getType()); + } + return true; + } + + private final class WebSocketMessageWriteCallback implements WriteCallback { + + @Override + public void writeFailed(Throwable x) { + webSocketMessageProcessor.cancel(); + webSocketMessageProcessor.onError(x); + } + + @Override + public void writeSuccess() { + webSocketMessageProcessor.setReady(true); + webSocketMessageProcessor.onWritePossible(); + } + + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java new file mode 100644 index 0000000000..82295c6e0a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.util.DecoratedObjectFactory; +import org.eclipse.jetty.websocket.server.WebSocketServerFactory; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.springframework.context.Lifecycle; +import org.springframework.core.NamedThreadLocal; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServletServerHttpRequest; +import org.springframework.http.server.reactive.ServletServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; +import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * A {@link RequestUpgradeStrategy} for use with Jetty. + * + * @author Violeta Georgieva + * @since 5.0 + */ +public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy, Lifecycle { + + private static final ThreadLocal wsContainerHolder = + new NamedThreadLocal<>("Jetty WebSocketHandler Adapter"); + + private WebSocketServerFactory factory; + + private ServletContext servletContext; + + private volatile boolean running = false; + + @Override + public Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler) { + + JettyWebSocketHandlerAdapter adapter = + new JettyWebSocketHandlerAdapter(webSocketHandler); + + HttpServletRequest servletRequest = getHttpServletRequest(exchange.getRequest()); + HttpServletResponse servletResponse = getHttpServletResponse(exchange.getResponse()); + + if (this.servletContext == null) { + this.servletContext = servletRequest.getServletContext(); + servletContext.setAttribute(DecoratedObjectFactory.ATTR, new DecoratedObjectFactory()); + } + + try { + start(); + + Assert.isTrue(this.factory.isUpgradeRequest(servletRequest, servletResponse), "Not a WebSocket handshake"); + + wsContainerHolder.set(adapter); + this.factory.acceptWebSocket(servletRequest, servletResponse); + } + catch (IOException ex) { + return Mono.error(ex); + } + finally { + wsContainerHolder.remove(); + } + + return Mono.empty(); + } + + @Override + public void start() { + if (!isRunning() && this.servletContext != null) { + this.running = true; + try { + this.factory = new WebSocketServerFactory(this.servletContext); + this.factory.setCreator(new WebSocketCreator() { + + @Override + public Object createWebSocket(ServletUpgradeRequest req, + ServletUpgradeResponse resp) { + JettyWebSocketHandlerAdapter adapter = wsContainerHolder.get(); + Assert.state(adapter != null, "Expected JettyWebSocketHandlerAdapter"); + return adapter; + } + + }); + this.factory.start(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to start Jetty WebSocketServerFactory", ex); + } + } + } + + @Override + public void stop() { + if (isRunning()) { + this.running = false; + try { + this.factory.stop(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to stop Jetty WebSocketServerFactory", ex); + } + } + } + + @Override + public boolean isRunning() { + return this.running; + } + + private final HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { + Assert.isTrue(request instanceof ServletServerHttpRequest); + return ((ServletServerHttpRequest) request).getServletRequest(); + } + + private final HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { + Assert.isTrue(response instanceof ServletServerHttpResponse); + return ((ServletServerHttpResponse) response).getServletResponse(); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java index 5d494253cb..17d7bb4970 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/socket/server/AbstractWebSocketHandlerIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.bootstrap.HttpServer; import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; +import org.springframework.http.server.reactive.bootstrap.JettyHttpServer; import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer; import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer; import org.springframework.http.server.reactive.bootstrap.UndertowHttpServer; @@ -39,6 +40,7 @@ import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; +import org.springframework.web.reactive.socket.server.upgrade.JettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.RxNettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; @@ -71,7 +73,8 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { {new ReactorHttpServer(), ReactorNettyConfig.class}, {new RxNettyHttpServer(), RxNettyConfig.class}, {new TomcatHttpServer(base.getAbsolutePath(), WsContextListener.class), TomcatConfig.class}, - {new UndertowHttpServer(), UndertowConfig.class} + {new UndertowHttpServer(), UndertowConfig.class}, + {new JettyHttpServer(), JettyConfig.class} }; } @@ -162,4 +165,13 @@ public abstract class AbstractWebSocketHandlerIntegrationTests { } } + @Configuration + static class JettyConfig extends AbstractHandlerAdapterConfig { + + @Override + protected RequestUpgradeStrategy getUpgradeStrategy() { + return new JettyRequestUpgradeStrategy(); + } + } + } From fe7ee5ff33af4ed494c09263d1a6764568601d9f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 9 Dec 2016 18:22:19 +0200 Subject: [PATCH 05/12] Rename "Request/ResponseBody" publisher/processor AbstractRequestBodyPublisher and AbstractResponseBodyProcessor are now used for WebSocket messages too and have been renamed more generally to AbstractListenerReadPublisher and AbstractListenerWriteProcessor. Issue: SPR-14527 --- ...stractListenerWebSocketSessionSupport.java | 8 +-- ...ava => AbstractListenerReadPublisher.java} | 52 ++++++++++--------- ...va => AbstractListenerWriteProcessor.java} | 36 +++++++------ .../reactive/ServletServerHttpRequest.java | 2 +- .../reactive/ServletServerHttpResponse.java | 2 +- .../reactive/UndertowServerHttpRequest.java | 2 +- .../reactive/UndertowServerHttpResponse.java | 2 +- ...s.java => ListenerReadPublisherTests.java} | 8 +-- 8 files changed, 58 insertions(+), 54 deletions(-) rename spring-web/src/main/java/org/springframework/http/server/reactive/{AbstractRequestBodyPublisher.java => AbstractListenerReadPublisher.java} (80%) rename spring-web/src/main/java/org/springframework/http/server/reactive/{AbstractResponseBodyProcessor.java => AbstractListenerWriteProcessor.java} (84%) rename spring-web/src/test/java/org/springframework/http/server/reactive/{AbstractRequestBodyPublisherTests.java => ListenerReadPublisherTests.java} (88%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java index 44fbe4bb84..dab993a0bc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java @@ -22,8 +22,8 @@ import java.net.URI; import java.util.concurrent.atomic.AtomicBoolean; import org.reactivestreams.Publisher; -import org.springframework.http.server.reactive.AbstractRequestBodyPublisher; -import org.springframework.http.server.reactive.AbstractResponseBodyProcessor; +import org.springframework.http.server.reactive.AbstractListenerReadPublisher; +import org.springframework.http.server.reactive.AbstractListenerWriteProcessor; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketMessage; @@ -122,7 +122,7 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock } } - final class WebSocketMessagePublisher extends AbstractRequestBodyPublisher { + final class WebSocketMessagePublisher extends AbstractListenerReadPublisher { private volatile WebSocketMessage webSocketMessage; @Override @@ -155,7 +155,7 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock } } - final class WebSocketMessageProcessor extends AbstractResponseBodyProcessor { + final class WebSocketMessageProcessor extends AbstractListenerWriteProcessor { private volatile boolean isReady = true; @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java similarity index 80% rename from spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java rename to spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java index cfc6fc6127..51146b4ee9 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerReadPublisher.java @@ -32,15 +32,17 @@ import reactor.core.publisher.Operators; /** * Abstract base class for {@code Publisher} implementations that bridge between - * event-listener APIs and Reactive Streams. Specifically, base class for the - * Servlet 3.1 and Undertow support. + * event-listener read APIs and Reactive Streams. Specifically, a base class for + * reading from the HTTP request body with Servlet 3.1 and Undertow as well as + * handling incoming WebSocket messages with JSR-356, Jetty, and Undertow. * * @author Arjen Poutsma + * @author Violeta Georgieva * @since 5.0 * @see ServletServerHttpRequest * @see UndertowHttpHandlerAdapter */ -public abstract class AbstractRequestBodyPublisher implements Publisher { +public abstract class AbstractListenerReadPublisher implements Publisher { protected final Log logger = LogFactory.getLog(getClass()); @@ -155,11 +157,11 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { } - private static final class RequestBodySubscription implements Subscription { + private static final class ReadSubscription implements Subscription { - private final AbstractRequestBodyPublisher publisher; + private final AbstractListenerReadPublisher publisher; - public RequestBodySubscription(AbstractRequestBodyPublisher publisher) { + public ReadSubscription(AbstractListenerReadPublisher publisher) { this.publisher = publisher; } @@ -207,15 +209,15 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { /** * The initial unsubscribed state. Will respond to {@link - * #subscribe(AbstractRequestBodyPublisher, Subscriber)} by + * #subscribe(AbstractListenerReadPublisher, Subscriber)} by * changing state to {@link #NO_DEMAND}. */ UNSUBSCRIBED { @Override - void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { + void subscribe(AbstractListenerReadPublisher publisher, Subscriber subscriber) { Objects.requireNonNull(subscriber); if (publisher.changeState(this, NO_DEMAND)) { - Subscription subscription = new RequestBodySubscription(publisher); + Subscription subscription = new ReadSubscription(publisher); publisher.subscriber = subscriber; subscriber.onSubscribe(subscription); } @@ -227,13 +229,13 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { /** * State that gets entered when there is no demand. Responds to {@link - * #request(AbstractRequestBodyPublisher, long)} by increasing the demand, + * #request(AbstractListenerReadPublisher, long)} by increasing the demand, * changing state to {@link #DEMAND} and will check whether there * is data available for reading. */ NO_DEMAND { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractListenerReadPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); if (publisher.changeState(this, DEMAND)) { @@ -245,20 +247,20 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { /** * State that gets entered when there is demand. Responds to - * {@link #onDataAvailable(AbstractRequestBodyPublisher)} by + * {@link #onDataAvailable(AbstractListenerReadPublisher)} by * reading the available data. The state will be changed to * {@link #NO_DEMAND} if there is no demand. */ DEMAND { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractListenerReadPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); } } @Override - void onDataAvailable(AbstractRequestBodyPublisher publisher) { + void onDataAvailable(AbstractListenerReadPublisher publisher) { if (publisher.changeState(this, READING)) { try { boolean demandAvailable = publisher.readAndPublish(); @@ -279,7 +281,7 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { READING { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractListenerReadPublisher publisher, long n) { if (Operators.checkRequest(n, publisher.subscriber)) { Operators.addAndGet(publisher.demand, n); } @@ -291,40 +293,40 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { */ COMPLETED { @Override - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractListenerReadPublisher publisher, long n) { // ignore } @Override - void cancel(AbstractRequestBodyPublisher publisher) { + void cancel(AbstractListenerReadPublisher publisher) { // ignore } @Override - void onAllDataRead(AbstractRequestBodyPublisher publisher) { + void onAllDataRead(AbstractListenerReadPublisher publisher) { // ignore } @Override - void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + void onError(AbstractListenerReadPublisher publisher, Throwable t) { // ignore } }; - void subscribe(AbstractRequestBodyPublisher publisher, Subscriber subscriber) { + void subscribe(AbstractListenerReadPublisher publisher, Subscriber subscriber) { throw new IllegalStateException(toString()); } - void request(AbstractRequestBodyPublisher publisher, long n) { + void request(AbstractListenerReadPublisher publisher, long n) { throw new IllegalStateException(toString()); } - void cancel(AbstractRequestBodyPublisher publisher) { + void cancel(AbstractListenerReadPublisher publisher) { publisher.changeState(this, COMPLETED); } - void onDataAvailable(AbstractRequestBodyPublisher publisher) { + void onDataAvailable(AbstractListenerReadPublisher publisher) { // ignore } - void onAllDataRead(AbstractRequestBodyPublisher publisher) { + void onAllDataRead(AbstractListenerReadPublisher publisher) { if (publisher.changeState(this, COMPLETED)) { if (publisher.subscriber != null) { publisher.subscriber.onComplete(); @@ -332,7 +334,7 @@ public abstract class AbstractRequestBodyPublisher implements Publisher { } } - void onError(AbstractRequestBodyPublisher publisher, Throwable t) { + void onError(AbstractListenerReadPublisher publisher, Throwable t) { if (publisher.changeState(this, COMPLETED)) { if (publisher.subscriber != null) { publisher.subscriber.onError(t); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java similarity index 84% rename from spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java rename to spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index a272c62a36..e4a97040c0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -33,16 +33,18 @@ import org.springframework.util.Assert; /** * Abstract base class for {@code Processor} implementations that bridge between - * event-listener APIs and Reactive Streams. Specifically, base class for the - * Servlet 3.1 and Undertow support. + * event-listener write APIs and Reactive Streams. Specifically, base class for + * writing to the HTTP response body with Servlet 3.1 and Undertow support as + * well for writing WebSocket messages with JSR-356, Jetty, and Undertow. * * @author Arjen Poutsma + * @author Violeta Georgieva * @since 5.0 * @see ServletServerHttpRequest * @see UndertowHttpHandlerAdapter * @see ServerHttpResponse#writeWith(Publisher) */ -public abstract class AbstractResponseBodyProcessor implements Processor { +public abstract class AbstractListenerWriteProcessor implements Processor { protected final Log logger = LogFactory.getLog(getClass()); @@ -190,7 +192,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onSubscribe(AbstractResponseBodyProcessor processor, Subscription subscription) { + public void onSubscribe(AbstractListenerWriteProcessor processor, Subscription subscription) { Objects.requireNonNull(subscription, "Subscription cannot be null"); if (processor.changeState(this, REQUESTED)) { processor.subscription = subscription; @@ -210,7 +212,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onNext(AbstractResponseBodyProcessor processor, T data) { + public void onNext(AbstractListenerWriteProcessor processor, T data) { if (processor.isDataEmpty(data)) { processor.subscription.request(1); } @@ -223,7 +225,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractListenerWriteProcessor processor) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishComplete(); } @@ -241,7 +243,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onWritePossible(AbstractResponseBodyProcessor processor) { + public void onWritePossible(AbstractListenerWriteProcessor processor) { if (processor.changeState(this, WRITING)) { T data = processor.currentData; try { @@ -270,7 +272,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractListenerWriteProcessor processor) { processor.subscriberCompleted = true; } }, @@ -281,7 +283,7 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractListenerWriteProcessor processor) { processor.subscriberCompleted = true; } }, @@ -291,40 +293,40 @@ public abstract class AbstractResponseBodyProcessor implements Processor void onNext(AbstractResponseBodyProcessor processor, T data) { + public void onNext(AbstractListenerWriteProcessor processor, T data) { // ignore } @Override - public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { + public void onError(AbstractListenerWriteProcessor processor, Throwable ex) { // ignore } @Override - public void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractListenerWriteProcessor processor) { // ignore } }; - public void onSubscribe(AbstractResponseBodyProcessor processor, Subscription subscription) { + public void onSubscribe(AbstractListenerWriteProcessor processor, Subscription subscription) { subscription.cancel(); } - public void onNext(AbstractResponseBodyProcessor processor, T data) { + public void onNext(AbstractListenerWriteProcessor processor, T data) { throw new IllegalStateException(toString()); } - public void onError(AbstractResponseBodyProcessor processor, Throwable ex) { + public void onError(AbstractListenerWriteProcessor processor, Throwable ex) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishError(ex); } } - public void onComplete(AbstractResponseBodyProcessor processor) { + public void onComplete(AbstractListenerWriteProcessor processor) { throw new IllegalStateException(toString()); } - public void onWritePossible(AbstractResponseBodyProcessor processor) { + public void onWritePossible(AbstractListenerWriteProcessor processor) { // ignore } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 58ee2f718b..08d204fed1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -203,7 +203,7 @@ public class ServletServerHttpRequest extends AbstractServerHttpRequest { } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + private static class RequestBodyPublisher extends AbstractListenerReadPublisher { private final RequestBodyPublisher.RequestBodyReadListener readListener = new RequestBodyPublisher.RequestBodyReadListener(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 20cc5a6019..753f8bb45b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -184,7 +184,7 @@ public class ServletServerHttpResponse extends AbstractListenerServerHttpRespons } - private class ResponseBodyProcessor extends AbstractResponseBodyProcessor { + private class ResponseBodyProcessor extends AbstractListenerWriteProcessor { private final ServletOutputStream outputStream; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 54dd95af8e..9065bae6a3 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -106,7 +106,7 @@ public class UndertowServerHttpRequest extends AbstractServerHttpRequest { return Flux.from(this.body); } - private static class RequestBodyPublisher extends AbstractRequestBodyPublisher { + private static class RequestBodyPublisher extends AbstractListenerReadPublisher { private final ChannelListener readListener = new ReadListener(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index aaefc898eb..2f97550371 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -139,7 +139,7 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon } - private static class ResponseBodyProcessor extends AbstractResponseBodyProcessor { + private static class ResponseBodyProcessor extends AbstractListenerWriteProcessor { private final ChannelListener listener = new WriteListener(); diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisherTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ListenerReadPublisherTests.java similarity index 88% rename from spring-web/src/test/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisherTests.java rename to spring-web/src/test/java/org/springframework/http/server/reactive/ListenerReadPublisherTests.java index cda831cc08..15a663f65f 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/AbstractRequestBodyPublisherTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ListenerReadPublisherTests.java @@ -32,12 +32,12 @@ import org.springframework.core.io.buffer.DataBuffer; import static org.junit.Assert.assertTrue; /** - * Unit tests for {@link AbstractRequestBodyPublisher} + * Unit tests for {@link AbstractListenerReadPublisher} * * @author Violeta Georgieva * @since 5.0 */ -public class AbstractRequestBodyPublisherTests { +public class ListenerReadPublisherTests { @Test public void testReceiveTwoRequestCallsWhenOnSubscribe() { @@ -45,14 +45,14 @@ public class AbstractRequestBodyPublisherTests { Subscriber subscriber = mock(Subscriber.class); doAnswer(new SubscriptionAnswer()).when(subscriber).onSubscribe(isA(Subscription.class)); - TestRequestBodyPublisher publisher = new TestRequestBodyPublisher(); + TestListenerReadPublisher publisher = new TestListenerReadPublisher(); publisher.subscribe(subscriber); publisher.onDataAvailable(); assertTrue(publisher.getReadCalls() == 2); } - private static final class TestRequestBodyPublisher extends AbstractRequestBodyPublisher { + private static final class TestListenerReadPublisher extends AbstractListenerReadPublisher { private int readCalls = 0; From d1411d9fc299f4f153fee44dd31cf758eb41e09c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 9 Dec 2016 20:38:56 +0200 Subject: [PATCH 06/12] Simple refactorings in AbstractListenerWebSocketSessionSupport Dropped "Support" from the name since it not only provides support methods but actually implements WebSocketSession. Renamed inner classes: WebSocketMessagePublisher -> WebSocketReceivePublisher WebSocketMessageProcessor -> WebSocketSendProcessor Add protected getter for sendProcessor. Reduce scoping: WebSocketReceivePublisher -> private WebSocketSendProcessor -> protected WebSocketSendProcessor#setReady -> public (class is still protected) A few more method name alignments and Javadoc updates. Issue: SPR-14527 --- ... => AbstractListenerWebSocketSession.java} | 93 ++++++++++--------- .../socket/adapter/JettyWebSocketSession.java | 16 ++-- .../TomcatWebSocketHandlerAdapter.java | 6 +- .../adapter/TomcatWebSocketSession.java | 20 ++-- .../adapter/UndertowWebSocketSession.java | 20 ++-- 5 files changed, 78 insertions(+), 77 deletions(-) rename spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/{AbstractListenerWebSocketSessionSupport.java => AbstractListenerWebSocketSession.java} (63%) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java similarity index 63% rename from spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java rename to spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index dab993a0bc..02b7a59169 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSessionSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -18,20 +18,19 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; import java.net.URI; - import java.util.concurrent.atomic.AtomicBoolean; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.http.server.reactive.AbstractListenerReadPublisher; import org.springframework.http.server.reactive.AbstractListenerWriteProcessor; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketMessage; -import org.springframework.web.reactive.socket.WebSocketSession; import org.springframework.web.reactive.socket.WebSocketMessage.Type; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; +import org.springframework.web.reactive.socket.WebSocketSession; /** * Base class for Listener-based {@link WebSocketSession} adapters. @@ -39,7 +38,7 @@ import reactor.core.publisher.Mono; * @author Violeta Georgieva * @since 5.0 */ -public abstract class AbstractListenerWebSocketSessionSupport extends WebSocketSessionSupport { +public abstract class AbstractListenerWebSocketSession extends WebSocketSessionSupport { private final AtomicBoolean sendCalled = new AtomicBoolean(); @@ -47,12 +46,12 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock private final URI uri; - protected final WebSocketMessagePublisher webSocketMessagePublisher = - new WebSocketMessagePublisher(); + private final WebSocketReceivePublisher receivePublisher = new WebSocketReceivePublisher(); - protected volatile WebSocketMessageProcessor webSocketMessageProcessor; + private volatile WebSocketSendProcessor sendProcessor; - public AbstractListenerWebSocketSessionSupport(T delegate, String id, URI uri) { + + public AbstractListenerWebSocketSession(T delegate, String id, URI uri) { super(delegate); Assert.notNull(id, "'id' is required."); Assert.notNull(uri, "'uri' is required."); @@ -60,6 +59,7 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock this.uri = uri; } + @Override public String getId() { return this.id; @@ -70,18 +70,22 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock return this.uri; } + protected WebSocketSendProcessor getSendProcessor() { + return this.sendProcessor; + } + @Override public Flux receive() { - return Flux.from(this.webSocketMessagePublisher); + return Flux.from(this.receivePublisher); } @Override public Mono send(Publisher messages) { if (this.sendCalled.compareAndSet(false, true)) { - this.webSocketMessageProcessor = new WebSocketMessageProcessor(); + this.sendProcessor = new WebSocketSendProcessor(); return Mono.from(subscriber -> { - messages.subscribe(this.webSocketMessageProcessor); - this.webSocketMessageProcessor.subscribe(subscriber); + messages.subscribe(this.sendProcessor); + this.sendProcessor.subscribe(subscriber); }); } else { @@ -97,32 +101,38 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock // no-op } - protected abstract boolean writeInternal(WebSocketMessage message) throws IOException; + protected boolean isReadyToReceive() { + return this.receivePublisher.isReadyToReceive(); + } - /** Handle a message callback from the Servlet container */ + protected abstract boolean sendMessage(WebSocketMessage message) throws IOException; + + /** Handle a message callback from the WebSocketHandler adapter */ void handleMessage(Type type, WebSocketMessage message) { - this.webSocketMessagePublisher.processWebSocketMessage(message); + this.receivePublisher.handleMessage(message); } - /** Handle a error callback from the Servlet container */ + /** Handle an error callback from the WebSocketHandler adapter */ void handleError(Throwable ex) { - this.webSocketMessagePublisher.onError(ex); - if (this.webSocketMessageProcessor != null) { - this.webSocketMessageProcessor.cancel(); - this.webSocketMessageProcessor.onError(ex); + this.receivePublisher.onError(ex); + if (this.sendProcessor != null) { + this.sendProcessor.cancel(); + this.sendProcessor.onError(ex); } } - /** Handle a complete callback from the Servlet container */ + /** Handle a close callback from the WebSocketHandler adapter */ void handleClose(CloseStatus reason) { - this.webSocketMessagePublisher.onAllDataRead(); - if (this.webSocketMessageProcessor != null) { - this.webSocketMessageProcessor.cancel(); - this.webSocketMessageProcessor.onComplete(); + this.receivePublisher.onAllDataRead(); + if (this.sendProcessor != null) { + this.sendProcessor.cancel(); + this.sendProcessor.onComplete(); } } - final class WebSocketMessagePublisher extends AbstractListenerReadPublisher { + + private final class WebSocketReceivePublisher extends AbstractListenerReadPublisher { + private volatile WebSocketMessage webSocketMessage; @Override @@ -144,52 +154,47 @@ public abstract class AbstractListenerWebSocketSessionSupport extends WebSock return null; } - void processWebSocketMessage(WebSocketMessage webSocketMessage) { + void handleMessage(WebSocketMessage webSocketMessage) { this.webSocketMessage = webSocketMessage; suspendReceives(); onDataAvailable(); } - boolean canAccept() { + boolean isReadyToReceive() { return this.webSocketMessage == null; } } - final class WebSocketMessageProcessor extends AbstractListenerWriteProcessor { + protected final class WebSocketSendProcessor extends AbstractListenerWriteProcessor { + private volatile boolean isReady = true; @Override protected boolean write(WebSocketMessage message) throws IOException { - return writeInternal(message); + return sendMessage(message); } @Override protected void releaseData() { if (logger.isTraceEnabled()) { - logger.trace("releaseBuffer: " + this.currentData); + logger.trace("releaseData: " + this.currentData); } this.currentData = null; } @Override - protected boolean isDataEmpty(WebSocketMessage data) { - return data.getPayload().readableByteCount() == 0; + protected boolean isDataEmpty(WebSocketMessage message) { + return message.getPayload().readableByteCount() == 0; } @Override protected boolean isWritePossible() { - if (this.isReady && this.currentData != null) { - return true; - } - else { - return false; - } + return this.isReady && this.currentData != null; } - void setReady(boolean ready) { + public void setReady(boolean ready) { this.isReady = ready; } - } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 0e7e330ef7..806f03295f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -35,7 +35,7 @@ import reactor.core.publisher.Mono; * @author Violeta Georgieva * @since 5.0 */ -public class JettyWebSocketSession extends AbstractListenerWebSocketSessionSupport { +public class JettyWebSocketSession extends AbstractListenerWebSocketSession { public JettyWebSocketSession(Session session) { super(session, ObjectUtils.getIdentityHexString(session), @@ -49,15 +49,15 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSessionSuppo } @Override - protected boolean writeInternal(WebSocketMessage message) throws IOException { + protected boolean sendMessage(WebSocketMessage message) throws IOException { if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); getDelegate().getRemote().sendString( new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), new WebSocketMessageWriteCallback()); } else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); getDelegate().getRemote().sendBytes(message.getPayload().asByteBuffer(), new WebSocketMessageWriteCallback()); } @@ -77,14 +77,14 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSessionSuppo @Override public void writeFailed(Throwable x) { - webSocketMessageProcessor.cancel(); - webSocketMessageProcessor.onError(x); + getSendProcessor().cancel(); + getSendProcessor().onError(x); } @Override public void writeSuccess() { - webSocketMessageProcessor.setReady(true); - webSocketMessageProcessor.onWritePossible(); + getSendProcessor().setReady(true); + getSendProcessor().onWritePossible(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java index 12fa2980e8..405ef5b388 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -65,7 +65,7 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(String message) { while (true) { - if (wsSession.canWebSocketMessagePublisherAccept()) { + if (wsSession.isReadyToReceive()) { WebSocketMessage wsMessage = toMessage(message); wsSession.handleMessage(wsMessage.getType(), wsMessage); break; @@ -79,7 +79,7 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(ByteBuffer message) { while (true) { - if (wsSession.canWebSocketMessagePublisherAccept()) { + if (wsSession.isReadyToReceive()) { WebSocketMessage wsMessage = toMessage(message); wsSession.handleMessage(wsMessage.getType(), wsMessage); break; @@ -93,7 +93,7 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(PongMessage message) { while (true) { - if (wsSession.canWebSocketMessagePublisherAccept()) { + if (wsSession.isReadyToReceive()) { WebSocketMessage wsMessage = toMessage(message); wsSession.handleMessage(wsMessage.getType(), wsMessage); break; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java index 3e9ab0258a..edc88015b5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java @@ -38,7 +38,7 @@ import reactor.core.publisher.Mono; * @author Violeta Georgieva * @since 5.0 */ -public class TomcatWebSocketSession extends AbstractListenerWebSocketSessionSupport { +public class TomcatWebSocketSession extends AbstractListenerWebSocketSession { public TomcatWebSocketSession(Session session) { super(session, session.getId(), session.getRequestURI()); @@ -56,20 +56,16 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSessionSupp return Mono.empty(); } - boolean canWebSocketMessagePublisherAccept() { - return this.webSocketMessagePublisher.canAccept(); - } - @Override - protected boolean writeInternal(WebSocketMessage message) throws IOException { + protected boolean sendMessage(WebSocketMessage message) throws IOException { if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); getDelegate().getAsyncRemote().sendText( new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), new WebSocketMessageSendHandler()); } else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); getDelegate().getAsyncRemote().sendBinary(message.getPayload().asByteBuffer(), new WebSocketMessageSendHandler()); } @@ -90,12 +86,12 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSessionSupp @Override public void onResult(SendResult result) { if (result.isOK()) { - webSocketMessageProcessor.setReady(true); - webSocketMessageProcessor.onWritePossible(); + getSendProcessor().setReady(true); + getSendProcessor().onWritePossible(); } else { - webSocketMessageProcessor.cancel(); - webSocketMessageProcessor.onError(result.getException()); + getSendProcessor().cancel(); + getSendProcessor().onError(result.getException()); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java index bacad80f64..b6f8a9dae9 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -39,7 +39,7 @@ import reactor.core.publisher.Mono; * @author Violeta Georgieva * @since 5.0 */ -public class UndertowWebSocketSession extends AbstractListenerWebSocketSessionSupport { +public class UndertowWebSocketSession extends AbstractListenerWebSocketSession { public UndertowWebSocketSession(WebSocketChannel channel) throws URISyntaxException { super(channel, ObjectUtils.getIdentityHexString(channel), new URI(channel.getUrl())); @@ -63,25 +63,25 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSessionSu } @Override - protected boolean writeInternal(WebSocketMessage message) throws IOException { + protected boolean sendMessage(WebSocketMessage message) throws IOException { if (WebSocketMessage.Type.TEXT.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); WebSockets.sendText( new String(message.getPayload().asByteBuffer().array(), StandardCharsets.UTF_8), getDelegate(), new WebSocketMessageSendHandler()); } else if (WebSocketMessage.Type.BINARY.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); WebSockets.sendBinary(message.getPayload().asByteBuffer(), getDelegate(), new WebSocketMessageSendHandler()); } else if (WebSocketMessage.Type.PING.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); WebSockets.sendPing(message.getPayload().asByteBuffer(), getDelegate(), new WebSocketMessageSendHandler()); } else if (WebSocketMessage.Type.PONG.equals(message.getType())) { - this.webSocketMessageProcessor.setReady(false); + getSendProcessor().setReady(false); WebSockets.sendPong(message.getPayload().asByteBuffer(), getDelegate(), new WebSocketMessageSendHandler()); } @@ -95,15 +95,15 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSessionSu @Override public void complete(WebSocketChannel channel, Void context) { - webSocketMessageProcessor.setReady(true); - webSocketMessageProcessor.onWritePossible(); + getSendProcessor().setReady(true); + getSendProcessor().onWritePossible(); } @Override public void onError(WebSocketChannel channel, Void context, Throwable throwable) { - webSocketMessageProcessor.cancel(); - webSocketMessageProcessor.onError(throwable); + getSendProcessor().cancel(); + getSendProcessor().onError(throwable); } } From 9d03b77cdc0dffa716b3f945814b48142992f4f0 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Fri, 9 Dec 2016 20:57:51 +0200 Subject: [PATCH 07/12] Rename "ResponseBody" flush processor + use generics Issue: SPR-14527 --- ...va => AbstractListenerFlushProcessor.java} | 44 +++++++++---------- .../AbstractListenerWriteProcessor.java | 2 +- .../reactive/ServletServerHttpResponse.java | 2 +- .../reactive/UndertowServerHttpResponse.java | 2 +- ...blisher.java => WriteResultPublisher.java} | 39 ++++++++-------- 5 files changed, 44 insertions(+), 45 deletions(-) rename spring-web/src/main/java/org/springframework/http/server/reactive/{AbstractResponseBodyFlushProcessor.java => AbstractListenerFlushProcessor.java} (74%) rename spring-web/src/main/java/org/springframework/http/server/reactive/{ResponseBodyWriteResultPublisher.java => WriteResultPublisher.java} (75%) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyFlushProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerFlushProcessor.java similarity index 74% rename from spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyFlushProcessor.java rename to spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerFlushProcessor.java index 56215d668a..b4d259e943 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractResponseBodyFlushProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerFlushProcessor.java @@ -27,8 +27,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.core.io.buffer.DataBuffer; - /** * Abstract base class for {@code Processor} implementations that bridge between * event-listener APIs and Reactive Streams. Specifically, base class for the @@ -41,11 +39,11 @@ import org.springframework.core.io.buffer.DataBuffer; * @see UndertowHttpHandlerAdapter * @see ServerHttpResponse#writeAndFlushWith(Publisher) */ -abstract class AbstractResponseBodyFlushProcessor implements Processor, Void> { +public abstract class AbstractListenerFlushProcessor implements Processor, Void> { protected final Log logger = LogFactory.getLog(getClass()); - private final ResponseBodyWriteResultPublisher resultPublisher = new ResponseBodyWriteResultPublisher(); + private final WriteResultPublisher resultPublisher = new WriteResultPublisher(); private final AtomicReference state = new AtomicReference<>(State.UNSUBSCRIBED); @@ -65,7 +63,7 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor publisher) { + public final void onNext(Publisher publisher) { if (logger.isTraceEnabled()) { logger.trace(this.state + " onNext: " + publisher); } @@ -100,7 +98,7 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor createBodyProcessor(); + protected abstract Processor createBodyProcessor(); /** * Flushes the output. @@ -130,7 +128,7 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor void onSubscribe(AbstractListenerFlushProcessor processor, Subscription subscription) { Objects.requireNonNull(subscription, "Subscription cannot be null"); if (processor.changeState(this, REQUESTED)) { processor.subscription = subscription; @@ -144,16 +142,16 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor chunk) { + public void onNext(AbstractListenerFlushProcessor processor, Publisher chunk) { if (processor.changeState(this, RECEIVED)) { - Processor chunkProcessor = processor.createBodyProcessor(); + Processor chunkProcessor = processor.createBodyProcessor(); chunk.subscribe(chunkProcessor); chunkProcessor.subscribe(new WriteSubscriber(processor)); } } @Override - public void onComplete(AbstractResponseBodyFlushProcessor processor) { + public void onComplete(AbstractListenerFlushProcessor processor) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishComplete(); } @@ -162,7 +160,7 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor void writeComplete(AbstractListenerFlushProcessor processor) { try { processor.flush(); } @@ -184,58 +182,58 @@ abstract class AbstractResponseBodyFlushProcessor implements Processor void onComplete(AbstractListenerFlushProcessor processor) { processor.subscriberCompleted = true; } }, COMPLETED { @Override - public void onNext(AbstractResponseBodyFlushProcessor processor, - Publisher publisher) { + public void onNext(AbstractListenerFlushProcessor processor, + Publisher publisher) { // ignore } @Override - public void onError(AbstractResponseBodyFlushProcessor processor, Throwable t) { + public void onError(AbstractListenerFlushProcessor processor, Throwable t) { // ignore } @Override - public void onComplete(AbstractResponseBodyFlushProcessor processor) { + public void onComplete(AbstractListenerFlushProcessor processor) { // ignore } }; - public void onSubscribe(AbstractResponseBodyFlushProcessor processor, Subscription subscription) { + public void onSubscribe(AbstractListenerFlushProcessor processor, Subscription subscription) { subscription.cancel(); } - public void onNext(AbstractResponseBodyFlushProcessor processor, Publisher publisher) { + public void onNext(AbstractListenerFlushProcessor processor, Publisher publisher) { throw new IllegalStateException(toString()); } - public void onError(AbstractResponseBodyFlushProcessor processor, Throwable ex) { + public void onError(AbstractListenerFlushProcessor processor, Throwable ex) { if (processor.changeState(this, COMPLETED)) { processor.resultPublisher.publishError(ex); } } - public void onComplete(AbstractResponseBodyFlushProcessor processor) { + public void onComplete(AbstractListenerFlushProcessor processor) { throw new IllegalStateException(toString()); } - public void writeComplete(AbstractResponseBodyFlushProcessor processor) { + public void writeComplete(AbstractListenerFlushProcessor processor) { // ignore } private static class WriteSubscriber implements Subscriber { - private final AbstractResponseBodyFlushProcessor processor; + private final AbstractListenerFlushProcessor processor; - public WriteSubscriber(AbstractResponseBodyFlushProcessor processor) { + public WriteSubscriber(AbstractListenerFlushProcessor processor) { this.processor = processor; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java index e4a97040c0..fb3b12a5fc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerWriteProcessor.java @@ -48,7 +48,7 @@ public abstract class AbstractListenerWriteProcessor implements Processor state = new AtomicReference<>(State.UNSUBSCRIBED); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 753f8bb45b..2d403add8f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -273,7 +273,7 @@ public class ServletServerHttpResponse extends AbstractListenerServerHttpRespons } - private class ResponseBodyFlushProcessor extends AbstractResponseBodyFlushProcessor { + private class ResponseBodyFlushProcessor extends AbstractListenerFlushProcessor { @Override protected Processor createBodyProcessor() { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index 2f97550371..0e1c950c21 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -218,7 +218,7 @@ public class UndertowServerHttpResponse extends AbstractListenerServerHttpRespon } } - private class ResponseBodyFlushProcessor extends AbstractResponseBodyFlushProcessor { + private class ResponseBodyFlushProcessor extends AbstractListenerFlushProcessor { @Override protected Processor createBodyProcessor() { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java similarity index 75% rename from spring-web/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java rename to spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java index 4a59fdcee9..f239b9426b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ResponseBodyWriteResultPublisher.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/WriteResultPublisher.java @@ -30,11 +30,12 @@ import reactor.core.publisher.Operators; * Publisher returned from {@link ServerHttpResponse#writeWith(Publisher)}. * * @author Arjen Poutsma + * @author Violeta Georgieva * @since 5.0 */ -class ResponseBodyWriteResultPublisher implements Publisher { +class WriteResultPublisher implements Publisher { - private static final Log logger = LogFactory.getLog(ResponseBodyWriteResultPublisher.class); + private static final Log logger = LogFactory.getLog(WriteResultPublisher.class); private final AtomicReference state = new AtomicReference<>(State.UNSUBSCRIBED); @@ -80,9 +81,9 @@ class ResponseBodyWriteResultPublisher implements Publisher { private static final class ResponseBodyWriteResultSubscription implements Subscription { - private final ResponseBodyWriteResultPublisher publisher; + private final WriteResultPublisher publisher; - public ResponseBodyWriteResultSubscription(ResponseBodyWriteResultPublisher publisher) { + public ResponseBodyWriteResultSubscription(WriteResultPublisher publisher) { this.publisher = publisher; } @@ -112,7 +113,7 @@ class ResponseBodyWriteResultPublisher implements Publisher { UNSUBSCRIBED { @Override - void subscribe(ResponseBodyWriteResultPublisher publisher, + void subscribe(WriteResultPublisher publisher, Subscriber subscriber) { Objects.requireNonNull(subscriber); if (publisher.changeState(this, SUBSCRIBED)) { @@ -132,28 +133,28 @@ class ResponseBodyWriteResultPublisher implements Publisher { } } @Override - void publishComplete(ResponseBodyWriteResultPublisher publisher) { + void publishComplete(WriteResultPublisher publisher) { publisher.publisherCompleted = true; } @Override - void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + void publishError(WriteResultPublisher publisher, Throwable t) { publisher.publisherError = t; } }, SUBSCRIBED { @Override - void request(ResponseBodyWriteResultPublisher publisher, long n) { + void request(WriteResultPublisher publisher, long n) { Operators.checkRequest(n, publisher.subscriber); } @Override - void publishComplete(ResponseBodyWriteResultPublisher publisher) { + void publishComplete(WriteResultPublisher publisher) { if (publisher.changeState(this, COMPLETED)) { publisher.subscriber.onComplete(); } } @Override - void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + void publishError(WriteResultPublisher publisher, Throwable t) { if (publisher.changeState(this, COMPLETED)) { publisher.subscriber.onError(t); } @@ -162,40 +163,40 @@ class ResponseBodyWriteResultPublisher implements Publisher { COMPLETED { @Override - void request(ResponseBodyWriteResultPublisher publisher, long n) { + void request(WriteResultPublisher publisher, long n) { // ignore } @Override - void cancel(ResponseBodyWriteResultPublisher publisher) { + void cancel(WriteResultPublisher publisher) { // ignore } @Override - void publishComplete(ResponseBodyWriteResultPublisher publisher) { + void publishComplete(WriteResultPublisher publisher) { // ignore } @Override - void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + void publishError(WriteResultPublisher publisher, Throwable t) { // ignore } }; - void subscribe(ResponseBodyWriteResultPublisher publisher, Subscriber subscriber) { + void subscribe(WriteResultPublisher publisher, Subscriber subscriber) { throw new IllegalStateException(toString()); } - void request(ResponseBodyWriteResultPublisher publisher, long n) { + void request(WriteResultPublisher publisher, long n) { throw new IllegalStateException(toString()); } - void cancel(ResponseBodyWriteResultPublisher publisher) { + void cancel(WriteResultPublisher publisher) { publisher.changeState(this, COMPLETED); } - void publishComplete(ResponseBodyWriteResultPublisher publisher) { + void publishComplete(WriteResultPublisher publisher) { throw new IllegalStateException(toString()); } - void publishError(ResponseBodyWriteResultPublisher publisher, Throwable t) { + void publishError(WriteResultPublisher publisher, Throwable t) { throw new IllegalStateException(toString()); } } From 08edec006b401e67e99cc33294b2c40639c42756 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Fri, 9 Dec 2016 21:35:00 +0200 Subject: [PATCH 08/12] Refactor AbstractListenerWebSocketSession - Added suspended flag to indicate whether the ReceivePublisher is able to process the incoming messages. - Use buffer strategy for the incoming messages. Issue: SPR-14527 --- .../AbstractListenerWebSocketSession.java | 18 ++++++------- .../adapter/JettyWebSocketHandlerAdapter.java | 6 ++--- .../TomcatWebSocketHandlerAdapter.java | 27 +++++++------------ .../adapter/UndertowWebSocketSession.java | 2 ++ ...BasicWebSocketHandlerIntegrationTests.java | 3 +-- 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index 02b7a59169..880ee61f2f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -40,6 +40,8 @@ import org.springframework.web.reactive.socket.WebSocketSession; */ public abstract class AbstractListenerWebSocketSession extends WebSocketSessionSupport { + private static final int BUFFER_SIZE = 8192; + private final AtomicBoolean sendCalled = new AtomicBoolean(); private final String id; @@ -50,6 +52,8 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi private volatile WebSocketSendProcessor sendProcessor; + private volatile boolean suspended = false; + public AbstractListenerWebSocketSession(T delegate, String id, URI uri) { super(delegate); @@ -76,7 +80,7 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi @Override public Flux receive() { - return Flux.from(this.receivePublisher); + return Flux.from(this.receivePublisher).onBackpressureBuffer(BUFFER_SIZE); } @Override @@ -94,15 +98,15 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi } protected void resumeReceives() { - // no-op + this.suspended = false; } protected void suspendReceives() { - // no-op + this.suspended = true; } - protected boolean isReadyToReceive() { - return this.receivePublisher.isReadyToReceive(); + protected boolean isSuspended() { + return this.suspended; } protected abstract boolean sendMessage(WebSocketMessage message) throws IOException; @@ -159,10 +163,6 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi suspendReceives(); onDataAvailable(); } - - boolean isReadyToReceive() { - return this.webSocketMessage == null; - } } protected final class WebSocketSendProcessor extends AbstractListenerWriteProcessor { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 815bd2da79..553f4402eb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -71,7 +71,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketMessage public void onWebSocketText(String message) { - if (this.wsSession != null) { + if (this.wsSession != null && !this.wsSession.isSuspended()) { WebSocketMessage wsMessage = toMessage(Type.TEXT, message); this.wsSession.handleMessage(wsMessage.getType(), wsMessage); } @@ -79,7 +79,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketMessage public void onWebSocketBinary(byte[] message, int offset, int length) { - if (this.wsSession != null) { + if (this.wsSession != null && !this.wsSession.isSuspended()) { WebSocketMessage wsMessage = toMessage(Type.BINARY, ByteBuffer.wrap(message, offset, length)); wsSession.handleMessage(wsMessage.getType(), wsMessage); } @@ -87,7 +87,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketFrame public void onWebSocketFrame(Frame frame) { - if (this.wsSession != null) { + if (this.wsSession != null && !this.wsSession.isSuspended()) { if (OpCode.PONG == frame.getOpCode()) { ByteBuffer message = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; WebSocketMessage wsMessage = toMessage(Type.PONG, message); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java index 405ef5b388..28319a9934 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -64,12 +64,9 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(String message) { - while (true) { - if (wsSession.isReadyToReceive()) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - break; - } + if (!wsSession.isSuspended()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); } } @@ -78,12 +75,9 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(ByteBuffer message) { - while (true) { - if (wsSession.isReadyToReceive()) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - break; - } + if (!wsSession.isSuspended()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); } } @@ -92,12 +86,9 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onMessage(PongMessage message) { - while (true) { - if (wsSession.isReadyToReceive()) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - break; - } + if (!wsSession.isSuspended()) { + WebSocketMessage wsMessage = toMessage(message); + wsSession.handleMessage(wsMessage.getType(), wsMessage); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java index b6f8a9dae9..dc3c305fca 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -55,10 +55,12 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession conn.write(messages .map(TextWebSocketFrame::new) - .cast(WebSocketFrame.class) - .concatWith(Observable.just(new CloseWebSocketFrame()))) + .cast(WebSocketFrame.class)) .cast(WebSocketFrame.class) .mergeWith(conn.getInput()) ) From db5bc4a24e001f0aadc13755afd4c86f511a1815 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Dec 2016 15:20:02 -0500 Subject: [PATCH 09/12] Minor refactoring of suspend/resumeReceiving suspend/resumeReceiving in the AbstractListenerWebSocketSession are now abstract methods. In Tomcat/Jetty these methods are no-op implementations that are then coupled with a buffering strategy via Flux#onBackpressureBuffer. In Undertow they rely on flow control for receiving WebSocket messages. Issue: SPR-14527 --- .../AbstractListenerWebSocketSession.java | 51 +++++++++++++------ .../adapter/JettyWebSocketHandlerAdapter.java | 6 +-- .../socket/adapter/JettyWebSocketSession.java | 22 +++++++- .../TomcatWebSocketHandlerAdapter.java | 18 +++---- .../adapter/TomcatWebSocketSession.java | 25 +++++++-- .../adapter/UndertowWebSocketSession.java | 21 ++++---- 6 files changed, 98 insertions(+), 45 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index 880ee61f2f..82035d4608 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -36,11 +36,17 @@ import org.springframework.web.reactive.socket.WebSocketSession; * Base class for Listener-based {@link WebSocketSession} adapters. * * @author Violeta Georgieva + * @author Rossen Stoyanchev * @since 5.0 */ public abstract class AbstractListenerWebSocketSession extends WebSocketSessionSupport { - private static final int BUFFER_SIZE = 8192; + /** + * The "back-pressure" buffer size to use if the underlying WebSocket API + * does not have flow control for receiving messages. + */ + private static final int RECEIVE_BUFFER_SIZE = 8192; + private final AtomicBoolean sendCalled = new AtomicBoolean(); @@ -52,8 +58,6 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi private volatile WebSocketSendProcessor sendProcessor; - private volatile boolean suspended = false; - public AbstractListenerWebSocketSession(T delegate, String id, URI uri) { super(delegate); @@ -80,7 +84,9 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi @Override public Flux receive() { - return Flux.from(this.receivePublisher).onBackpressureBuffer(BUFFER_SIZE); + return canSuspendReceiving() ? + Flux.from(this.receivePublisher) : + Flux.from(this.receivePublisher).onBackpressureBuffer(RECEIVE_BUFFER_SIZE); } @Override @@ -97,18 +103,33 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi } } - protected void resumeReceives() { - this.suspended = false; - } + /** + * Resume receiving new message(s) after demand is generated by the + * downstream Subscriber. + *

Note: if the underlying WebSocket API does not provide + * flow control for receiving messages, and this method should be a no-op + * and {@link #canSuspendReceiving()} should return {@code false}. + */ + protected abstract void resumeReceiving(); - protected void suspendReceives() { - this.suspended = true; - } + /** + * Suspend receiving until received message(s) are processed and more demand + * is generated by the downstream Subscriber. + *

Note: if the underlying WebSocket API does not provide + * flow control for receiving messages, and this method should be a no-op + * and {@link #canSuspendReceiving()} should return {@code false}. + */ + protected abstract void suspendReceiving(); - protected boolean isSuspended() { - return this.suspended; - } + /** + * Whether the underlying WebSocket API has flow control and can suspend and + * resume the receiving of messages. + */ + protected abstract boolean canSuspendReceiving(); + /** + * Send the given WebSocket message. + */ protected abstract boolean sendMessage(WebSocketMessage message) throws IOException; /** Handle a message callback from the WebSocketHandler adapter */ @@ -151,7 +172,7 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi if (this.webSocketMessage != null) { WebSocketMessage result = this.webSocketMessage; this.webSocketMessage = null; - resumeReceives(); + resumeReceiving(); return result; } @@ -160,7 +181,7 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi void handleMessage(WebSocketMessage webSocketMessage) { this.webSocketMessage = webSocketMessage; - suspendReceives(); + suspendReceiving(); onDataAvailable(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 553f4402eb..815bd2da79 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -71,7 +71,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketMessage public void onWebSocketText(String message) { - if (this.wsSession != null && !this.wsSession.isSuspended()) { + if (this.wsSession != null) { WebSocketMessage wsMessage = toMessage(Type.TEXT, message); this.wsSession.handleMessage(wsMessage.getType(), wsMessage); } @@ -79,7 +79,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketMessage public void onWebSocketBinary(byte[] message, int offset, int length) { - if (this.wsSession != null && !this.wsSession.isSuspended()) { + if (this.wsSession != null) { WebSocketMessage wsMessage = toMessage(Type.BINARY, ByteBuffer.wrap(message, offset, length)); wsSession.handleMessage(wsMessage.getType(), wsMessage); } @@ -87,7 +87,7 @@ public class JettyWebSocketHandlerAdapter { @OnWebSocketFrame public void onWebSocketFrame(Frame frame) { - if (this.wsSession != null && !this.wsSession.isSuspended()) { + if (this.wsSession != null) { if (OpCode.PONG == frame.getOpCode()) { ByteBuffer message = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; WebSocketMessage wsMessage = toMessage(Type.PONG, message); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 806f03295f..56f04611c4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -21,13 +21,13 @@ import java.nio.charset.StandardCharsets; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WriteCallback; +import reactor.core.publisher.Mono; + import org.springframework.util.ObjectUtils; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketMessage; import org.springframework.web.reactive.socket.WebSocketSession; -import reactor.core.publisher.Mono; - /** * Spring {@link WebSocketSession} adapter for Jetty's * {@link org.eclipse.jetty.websocket.api.Session}. @@ -37,11 +37,13 @@ import reactor.core.publisher.Mono; */ public class JettyWebSocketSession extends AbstractListenerWebSocketSession { + public JettyWebSocketSession(Session session) { super(session, ObjectUtils.getIdentityHexString(session), session.getUpgradeRequest().getRequestURI()); } + @Override protected Mono closeInternal(CloseStatus status) { getDelegate().close(status.getCode(), status.getReason()); @@ -73,6 +75,22 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession { + public TomcatWebSocketSession(Session session) { super(session, session.getId(), session.getRequestURI()); } + @Override protected Mono closeInternal(CloseStatus status) { try { @@ -81,6 +82,22 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSession Date: Mon, 12 Dec 2016 16:51:54 -0500 Subject: [PATCH 10/12] Polish --- .../AbstractListenerWebSocketSession.java | 7 +- .../adapter/JettyWebSocketHandlerAdapter.java | 72 +++++++++------- .../socket/adapter/JettyWebSocketSession.java | 22 ++--- .../TomcatWebSocketHandlerAdapter.java | 84 ++++++++----------- .../adapter/TomcatWebSocketSession.java | 22 ++--- .../UndertowWebSocketHandlerAdapter.java | 59 ++++++------- .../adapter/UndertowWebSocketSession.java | 35 ++++---- 7 files changed, 151 insertions(+), 150 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index 82035d4608..dbb4c53028 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -213,7 +213,12 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi return this.isReady && this.currentData != null; } - public void setReady(boolean ready) { + /** + * Sub-classes can invoke this before sending a message (false) and + * after receiving the async send callback (true) effective translating + * async completion callback into simple flow control. + */ + public void setReadyToSend(boolean ready) { this.isReady = ready; } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 815bd2da79..9314dfe85d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -30,6 +30,8 @@ import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.OpCode; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; + +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.util.Assert; @@ -50,84 +52,90 @@ public class JettyWebSocketHandlerAdapter { private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); - private final WebSocketHandler handler; + private final WebSocketHandler delegate; - private JettyWebSocketSession wsSession; + private JettyWebSocketSession session; - public JettyWebSocketHandlerAdapter(WebSocketHandler handler) { - Assert.notNull("'handler' is required"); - this.handler = handler; + + public JettyWebSocketHandlerAdapter(WebSocketHandler delegate) { + Assert.notNull("WebSocketHandler is required"); + this.delegate = delegate; } + @OnWebSocketConnect public void onWebSocketConnect(Session session) { - this.wsSession = new JettyWebSocketSession(session); + this.session = new JettyWebSocketSession(session); - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); - this.handler.handle(this.wsSession).subscribe(resultSubscriber); + HandlerResultSubscriber subscriber = new HandlerResultSubscriber(); + this.delegate.handle(this.session).subscribe(subscriber); } @OnWebSocketMessage public void onWebSocketText(String message) { - if (this.wsSession != null) { - WebSocketMessage wsMessage = toMessage(Type.TEXT, message); - this.wsSession.handleMessage(wsMessage.getType(), wsMessage); + if (this.session != null) { + WebSocketMessage webSocketMessage = toMessage(Type.TEXT, message); + this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); } } @OnWebSocketMessage public void onWebSocketBinary(byte[] message, int offset, int length) { - if (this.wsSession != null) { - WebSocketMessage wsMessage = toMessage(Type.BINARY, ByteBuffer.wrap(message, offset, length)); - wsSession.handleMessage(wsMessage.getType(), wsMessage); + if (this.session != null) { + ByteBuffer buffer = ByteBuffer.wrap(message, offset, length); + WebSocketMessage webSocketMessage = toMessage(Type.BINARY, buffer); + session.handleMessage(webSocketMessage.getType(), webSocketMessage); } } @OnWebSocketFrame public void onWebSocketFrame(Frame frame) { - if (this.wsSession != null) { + if (this.session != null) { if (OpCode.PONG == frame.getOpCode()) { - ByteBuffer message = frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD; - WebSocketMessage wsMessage = toMessage(Type.PONG, message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); + ByteBuffer buffer = (frame.getPayload() != null ? frame.getPayload() : EMPTY_PAYLOAD); + WebSocketMessage webSocketMessage = toMessage(Type.PONG, buffer); + session.handleMessage(webSocketMessage.getType(), webSocketMessage); } } } @OnWebSocketClose public void onWebSocketClose(int statusCode, String reason) { - if (this.wsSession != null) { - this.wsSession.handleClose(new CloseStatus(statusCode, reason)); + if (this.session != null) { + this.session.handleClose(new CloseStatus(statusCode, reason)); } } @OnWebSocketError public void onWebSocketError(Throwable cause) { - if (this.wsSession != null) { - this.wsSession.handleError(cause); + if (this.session != null) { + this.session.handleError(cause); } } private WebSocketMessage toMessage(Type type, T message) { if (Type.TEXT.equals(type)) { - return WebSocketMessage.create(Type.TEXT, - bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = this.bufferFactory.wrap(bytes); + return WebSocketMessage.create(Type.TEXT, buffer); } else if (Type.BINARY.equals(type)) { - return WebSocketMessage.create(Type.BINARY, - bufferFactory.wrap((ByteBuffer) message)); + DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); + return WebSocketMessage.create(Type.BINARY, buffer); } else if (Type.PONG.equals(type)) { - return WebSocketMessage.create(Type.PONG, - bufferFactory.wrap((ByteBuffer) message)); + DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); + return WebSocketMessage.create(Type.PONG, buffer); } else { throw new IllegalArgumentException("Unexpected message type: " + message); } } + private final class HandlerResultSubscriber implements Subscriber { @Override @@ -142,15 +150,15 @@ public class JettyWebSocketHandlerAdapter { @Override public void onError(Throwable ex) { - if (wsSession != null) { - wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + if (session != null) { + session.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); } } @Override public void onComplete() { - if (wsSession != null) { - wsSession.close(); + if (session != null) { + session.close(); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 56f04611c4..7f23241f53 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.websocket.api.Session; @@ -52,22 +53,21 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession() { - - @Override - public void onMessage(String message) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - } + this.session = new TomcatWebSocketSession(session); + session.addMessageHandler(String.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); }); - session.addMessageHandler(new MessageHandler.Whole() { - - @Override - public void onMessage(ByteBuffer message) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - } - + session.addMessageHandler(ByteBuffer.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); }); - session.addMessageHandler(new MessageHandler.Whole() { - - @Override - public void onMessage(PongMessage message) { - WebSocketMessage wsMessage = toMessage(message); - wsSession.handleMessage(wsMessage.getType(), wsMessage); - } - + session.addMessageHandler(PongMessage.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); }); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); - this.handler.handle(this.wsSession).subscribe(resultSubscriber); + this.delegate.handle(this.session).subscribe(resultSubscriber); } @Override public void onClose(Session session, CloseReason reason) { - if (this.wsSession != null) { - this.wsSession.handleClose( - new CloseStatus(reason.getCloseCode().getCode(), reason.getReasonPhrase())); + if (this.session != null) { + int code = reason.getCloseCode().getCode(); + this.session.handleClose(new CloseStatus(code, reason.getReasonPhrase())); } } @Override public void onError(Session session, Throwable exception) { - if (this.wsSession != null) { - this.wsSession.handleError(exception); + if (this.session != null) { + this.session.handleError(exception); } } private WebSocketMessage toMessage(T message) { if (message instanceof String) { - return WebSocketMessage.create(Type.TEXT, - bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); + return WebSocketMessage.create(Type.TEXT, this.bufferFactory.wrap(bytes)); } else if (message instanceof ByteBuffer) { - return WebSocketMessage.create(Type.BINARY, - bufferFactory.wrap((ByteBuffer) message)); + DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); + return WebSocketMessage.create(Type.BINARY, buffer); } else if (message instanceof PongMessage) { - return WebSocketMessage.create(Type.PONG, - bufferFactory.wrap(((PongMessage) message).getApplicationData())); + DataBuffer buffer = this.bufferFactory.wrap(((PongMessage) message).getApplicationData()); + return WebSocketMessage.create(Type.PONG, buffer); } else { throw new IllegalArgumentException("Unexpected message type: " + message); } } + private final class HandlerResultSubscriber implements Subscriber { @Override @@ -139,15 +127,15 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onError(Throwable ex) { - if (wsSession != null) { - wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + if (session != null) { + session.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); } } @Override public void onComplete() { - if (wsSession != null) { - wsSession.close(); + if (session != null) { + session.close(); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java index 6c11938ed2..a9698f6962 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import javax.websocket.CloseReason; import javax.websocket.CloseReason.CloseCodes; @@ -59,22 +60,21 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSession WebSocketMessage toMessage(Type type, T message) { if (Type.TEXT.equals(type)) { - return WebSocketMessage.create(Type.TEXT, - bufferFactory.wrap(((String) message).getBytes(StandardCharsets.UTF_8))); + byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); + return WebSocketMessage.create(Type.TEXT, bufferFactory.wrap(bytes)); } else if (Type.BINARY.equals(type)) { - return WebSocketMessage.create(Type.BINARY, - bufferFactory.allocateBuffer().write((ByteBuffer[]) message)); + DataBuffer buffer = bufferFactory.allocateBuffer().write((ByteBuffer[]) message); + return WebSocketMessage.create(Type.BINARY, buffer); } else if (Type.PONG.equals(type)) { - return WebSocketMessage.create(Type.PONG, - bufferFactory.allocateBuffer().write((ByteBuffer[]) message)); + DataBuffer buffer = bufferFactory.allocateBuffer().write((ByteBuffer[]) message); + return WebSocketMessage.create(Type.PONG, buffer); } else { throw new IllegalArgumentException("Unexpected message type: " + message); @@ -144,12 +145,12 @@ public class UndertowWebSocketHandlerAdapter implements WebSocketConnectionCallb @Override public void onError(Throwable ex) { - wsSession.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + session.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); } @Override public void onComplete() { - wsSession.close(); + session.close(); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java index eaee5879df..c2a3a15b06 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import io.undertow.websockets.core.CloseMessage; @@ -41,10 +42,12 @@ import org.springframework.web.reactive.socket.WebSocketSession; */ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession { + public UndertowWebSocketSession(WebSocketChannel channel) throws URISyntaxException { super(channel, ObjectUtils.getIdentityHexString(channel), new URI(channel.getUrl())); } + @Override protected Mono closeInternal(CloseStatus status) { CloseMessage cm = new CloseMessage(status.getCode(), status.getReason()); @@ -69,26 +72,23 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession { + + private final class SendProcessorCallback implements WebSocketCallback { @Override public void complete(WebSocketChannel channel, Void context) { - getSendProcessor().setReady(true); + getSendProcessor().setReadyToSend(true); getSendProcessor().onWritePossible(); } @Override - public void onError(WebSocketChannel channel, Void context, - Throwable throwable) { + public void onError(WebSocketChannel channel, Void context, Throwable throwable) { getSendProcessor().cancel(); getSendProcessor().onError(throwable); } - } } From 5829e1c1411a22b8c17e7c3f5d7a26a537a39dfe Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Dec 2016 17:02:18 -0500 Subject: [PATCH 11/12] Polish method and field declaration order --- .../AbstractListenerWebSocketSession.java | 25 ++++++----- .../adapter/JettyWebSocketHandlerAdapter.java | 32 +++++++------- .../socket/adapter/JettyWebSocketSession.java | 30 ++++++------- .../TomcatWebSocketHandlerAdapter.java | 34 +++++++-------- .../adapter/TomcatWebSocketSession.java | 42 +++++++++---------- .../UndertowWebSocketHandlerAdapter.java | 4 +- .../adapter/UndertowWebSocketSession.java | 26 ++++++------ 7 files changed, 98 insertions(+), 95 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java index dbb4c53028..40aad9df42 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/AbstractListenerWebSocketSession.java @@ -48,8 +48,6 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi private static final int RECEIVE_BUFFER_SIZE = 8192; - private final AtomicBoolean sendCalled = new AtomicBoolean(); - private final String id; private final URI uri; @@ -58,6 +56,8 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi private volatile WebSocketSendProcessor sendProcessor; + private final AtomicBoolean sendCalled = new AtomicBoolean(); + public AbstractListenerWebSocketSession(T delegate, String id, URI uri) { super(delegate); @@ -104,13 +104,10 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi } /** - * Resume receiving new message(s) after demand is generated by the - * downstream Subscriber. - *

Note: if the underlying WebSocket API does not provide - * flow control for receiving messages, and this method should be a no-op - * and {@link #canSuspendReceiving()} should return {@code false}. + * Whether the underlying WebSocket API has flow control and can suspend and + * resume the receiving of messages. */ - protected abstract void resumeReceiving(); + protected abstract boolean canSuspendReceiving(); /** * Suspend receiving until received message(s) are processed and more demand @@ -122,16 +119,22 @@ public abstract class AbstractListenerWebSocketSession extends WebSocketSessi protected abstract void suspendReceiving(); /** - * Whether the underlying WebSocket API has flow control and can suspend and - * resume the receiving of messages. + * Resume receiving new message(s) after demand is generated by the + * downstream Subscriber. + *

Note: if the underlying WebSocket API does not provide + * flow control for receiving messages, and this method should be a no-op + * and {@link #canSuspendReceiving()} should return {@code false}. */ - protected abstract boolean canSuspendReceiving(); + protected abstract void resumeReceiving(); /** * Send the given WebSocket message. */ protected abstract boolean sendMessage(WebSocketMessage message) throws IOException; + + // WebSocketHandler adapter delegate methods + /** Handle a message callback from the WebSocketHandler adapter */ void handleMessage(Type type, WebSocketMessage message) { this.receivePublisher.handleMessage(message); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 9314dfe85d..4d5c08816f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -53,12 +53,12 @@ public class JettyWebSocketHandlerAdapter { private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); - private final WebSocketHandler delegate; private JettyWebSocketSession session; + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public JettyWebSocketHandlerAdapter(WebSocketHandler delegate) { Assert.notNull("WebSocketHandler is required"); @@ -102,20 +102,6 @@ public class JettyWebSocketHandlerAdapter { } } - @OnWebSocketClose - public void onWebSocketClose(int statusCode, String reason) { - if (this.session != null) { - this.session.handleClose(new CloseStatus(statusCode, reason)); - } - } - - @OnWebSocketError - public void onWebSocketError(Throwable cause) { - if (this.session != null) { - this.session.handleError(cause); - } - } - private WebSocketMessage toMessage(Type type, T message) { if (Type.TEXT.equals(type)) { byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); @@ -135,6 +121,20 @@ public class JettyWebSocketHandlerAdapter { } } + @OnWebSocketClose + public void onWebSocketClose(int statusCode, String reason) { + if (this.session != null) { + this.session.handleClose(new CloseStatus(statusCode, reason)); + } + } + + @OnWebSocketError + public void onWebSocketError(Throwable cause) { + if (this.session != null) { + this.session.handleError(cause); + } + } + private final class HandlerResultSubscriber implements Subscriber { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 7f23241f53..5f83418870 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -46,9 +46,18 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { - getDelegate().close(status.getCode(), status.getReason()); - return Mono.empty(); + protected boolean canSuspendReceiving() { + return false; + } + + @Override + protected void suspendReceiving() { + // No-op + } + + @Override + protected void resumeReceiving() { + // No-op } @Override @@ -76,18 +85,9 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { + getDelegate().close(status.getCode(), status.getReason()); + return Mono.empty(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java index 042c7da643..f1913a798d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -45,12 +45,12 @@ import org.springframework.web.reactive.socket.WebSocketMessage.Type; */ public class TomcatWebSocketHandlerAdapter extends Endpoint { - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); - private final WebSocketHandler delegate; private TomcatWebSocketSession session; + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public TomcatWebSocketHandlerAdapter(WebSocketHandler delegate) { Assert.notNull("WebSocketHandler is required"); @@ -79,21 +79,6 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { this.delegate.handle(this.session).subscribe(resultSubscriber); } - @Override - public void onClose(Session session, CloseReason reason) { - if (this.session != null) { - int code = reason.getCloseCode().getCode(); - this.session.handleClose(new CloseStatus(code, reason.getReasonPhrase())); - } - } - - @Override - public void onError(Session session, Throwable exception) { - if (this.session != null) { - this.session.handleError(exception); - } - } - private WebSocketMessage toMessage(T message) { if (message instanceof String) { byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); @@ -112,6 +97,21 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { } } + @Override + public void onClose(Session session, CloseReason reason) { + if (this.session != null) { + int code = reason.getCloseCode().getCode(); + this.session.handleClose(new CloseStatus(code, reason.getReasonPhrase())); + } + } + + @Override + public void onError(Session session, Throwable exception) { + if (this.session != null) { + this.session.handleError(exception); + } + } + private final class HandlerResultSubscriber implements Subscriber { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java index a9698f6962..80aed582e3 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketSession.java @@ -47,15 +47,18 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { - try { - getDelegate().close( - new CloseReason(CloseCodes.getCloseCode(status.getCode()), status.getReason())); - } - catch (IOException e) { - return Mono.error(e); - } - return Mono.empty(); + protected boolean canSuspendReceiving() { + return false; + } + + @Override + protected void suspendReceiving() { + // No-op + } + + @Override + protected void resumeReceiving() { + // No-op } @Override @@ -83,18 +86,15 @@ public class TomcatWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { + try { + CloseReason.CloseCode code = CloseCodes.getCloseCode(status.getCode()); + getDelegate().close(new CloseReason(code, status.getReason())); + } + catch (IOException e) { + return Mono.error(e); + } + return Mono.empty(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java index 904c23e8bc..a0a17c3716 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java @@ -49,12 +49,12 @@ import io.undertow.websockets.spi.WebSocketHttpExchange; */ public class UndertowWebSocketHandlerAdapter implements WebSocketConnectionCallback { - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); - private final WebSocketHandler delegate; private UndertowWebSocketSession session; + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public UndertowWebSocketHandlerAdapter(WebSocketHandler delegate) { Assert.notNull("WebSocketHandler is required"); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java index c2a3a15b06..6d0b557753 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -49,25 +49,16 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { - CloseMessage cm = new CloseMessage(status.getCode(), status.getReason()); - if (!getDelegate().isCloseFrameSent()) { - WebSockets.sendClose(cm, getDelegate(), null); - } - return Mono.empty(); - } - - protected void resumeReceiving() { - getDelegate().resumeReceives(); + protected boolean canSuspendReceiving() { + return true; } protected void suspendReceiving() { getDelegate().suspendReceives(); } - @Override - protected boolean canSuspendReceiving() { - return true; + protected void resumeReceiving() { + getDelegate().resumeReceives(); } @Override @@ -96,6 +87,15 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession closeInternal(CloseStatus status) { + CloseMessage cm = new CloseMessage(status.getCode(), status.getReason()); + if (!getDelegate().isCloseFrameSent()) { + WebSockets.sendClose(cm, getDelegate(), null); + } + return Mono.empty(); + } + private final class SendProcessorCallback implements WebSocketCallback { From d6895aa09846a0e1e1904d17f00ea53c73fc104f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 12 Dec 2016 17:54:24 -0500 Subject: [PATCH 12/12] Consistently extend WebSocketHandlerAdapterSupport The WebSocketHandler adapters for all runtimes now extend WebSocketHandlerAdapterSupport, which now also exposes a shared DataBufferFactory property initialized from the response. Issue: SPR-14527 --- .../adapter/JettyWebSocketHandlerAdapter.java | 25 ++-- .../ReactorNettyWebSocketHandlerAdapter.java | 13 +- .../RxNettyWebSocketHandlerAdapter.java | 12 +- .../TomcatWebSocketHandlerAdapter.java | 131 +++++++++--------- .../UndertowWebSocketHandlerAdapter.java | 52 +++---- .../adapter/UndertowWebSocketSession.java | 5 +- .../WebSocketHandlerAdapterSupport.java | 26 +++- .../upgrade/JettyRequestUpgradeStrategy.java | 92 ++++++------ .../upgrade/TomcatRequestUpgradeStrategy.java | 51 +++---- .../UndertowRequestUpgradeStrategy.java | 26 ++-- 10 files changed, 205 insertions(+), 228 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java index 4d5c08816f..a7e9c17c46 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketHandlerAdapter.java @@ -32,9 +32,8 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.util.Assert; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketMessage; @@ -48,21 +47,17 @@ import org.springframework.web.reactive.socket.WebSocketMessage.Type; * @since 5.0 */ @WebSocket -public class JettyWebSocketHandlerAdapter { +public class JettyWebSocketHandlerAdapter extends WebSocketHandlerAdapterSupport { private static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); - - private final WebSocketHandler delegate; - private JettyWebSocketSession session; - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public JettyWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler delegate) { - public JettyWebSocketHandlerAdapter(WebSocketHandler delegate) { - Assert.notNull("WebSocketHandler is required"); - this.delegate = delegate; + super(request, response, delegate); } @@ -71,7 +66,7 @@ public class JettyWebSocketHandlerAdapter { this.session = new JettyWebSocketSession(session); HandlerResultSubscriber subscriber = new HandlerResultSubscriber(); - this.delegate.handle(this.session).subscribe(subscriber); + getDelegate().handle(this.session).subscribe(subscriber); } @OnWebSocketMessage @@ -105,15 +100,15 @@ public class JettyWebSocketHandlerAdapter { private WebSocketMessage toMessage(Type type, T message) { if (Type.TEXT.equals(type)) { byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = this.bufferFactory.wrap(bytes); + DataBuffer buffer = getBufferFactory().wrap(bytes); return WebSocketMessage.create(Type.TEXT, buffer); } else if (Type.BINARY.equals(type)) { - DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); + DataBuffer buffer = getBufferFactory().wrap((ByteBuffer) message); return WebSocketMessage.create(Type.BINARY, buffer); } else if (Type.PONG.equals(type)) { - DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); + DataBuffer buffer = getBufferFactory().wrap((ByteBuffer) message); return WebSocketMessage.create(Type.PONG, buffer); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/ReactorNettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/ReactorNettyWebSocketHandlerAdapter.java index faaf90c2b5..56b6e77994 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/ReactorNettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/ReactorNettyWebSocketHandlerAdapter.java @@ -21,10 +21,8 @@ import org.reactivestreams.Publisher; import reactor.ipc.netty.http.HttpInbound; import reactor.ipc.netty.http.HttpOutbound; -import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.Assert; import org.springframework.web.reactive.socket.WebSocketHandler; /** @@ -38,22 +36,13 @@ public class ReactorNettyWebSocketHandlerAdapter extends WebSocketHandlerAdapter implements BiFunction> { - private final NettyDataBufferFactory bufferFactory; - - public ReactorNettyWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) { - super(request, handler); - Assert.notNull("'response' is required"); - this.bufferFactory = (NettyDataBufferFactory) response.bufferFactory(); + super(request, response, handler); } - public NettyDataBufferFactory getBufferFactory() { - return this.bufferFactory; - } - @Override public Publisher apply(HttpInbound inbound, HttpOutbound outbound) { ReactorNettyWebSocketSession session = diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/RxNettyWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/RxNettyWebSocketHandlerAdapter.java index 772de5c71e..d9896ad43c 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/RxNettyWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/RxNettyWebSocketHandlerAdapter.java @@ -20,10 +20,8 @@ import reactor.core.publisher.Mono; import rx.Observable; import rx.RxReactiveStreams; -import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.util.Assert; import org.springframework.web.reactive.socket.WebSocketHandler; /** @@ -36,22 +34,14 @@ import org.springframework.web.reactive.socket.WebSocketHandler; public class RxNettyWebSocketHandlerAdapter extends WebSocketHandlerAdapterSupport implements io.reactivex.netty.protocol.http.ws.server.WebSocketHandler { - private final NettyDataBufferFactory bufferFactory; - public RxNettyWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) { - super(request, handler); - Assert.notNull("'response' is required"); - this.bufferFactory = (NettyDataBufferFactory) response.bufferFactory(); + super(request, response, handler); } - public NettyDataBufferFactory getBufferFactory() { - return this.bufferFactory; - } - @Override public Observable handle(WebSocketConnection conn) { RxNettyWebSocketSession session = new RxNettyWebSocketSession(conn, getUri(), getBufferFactory()); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java index f1913a798d..74b65f7b84 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/TomcatWebSocketHandlerAdapter.java @@ -28,9 +28,8 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.util.Assert; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.reactive.socket.CloseStatus; import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketMessage; @@ -43,76 +42,84 @@ import org.springframework.web.reactive.socket.WebSocketMessage.Type; * @author Violeta Georgieva * @since 5.0 */ -public class TomcatWebSocketHandlerAdapter extends Endpoint { - - private final WebSocketHandler delegate; +public class TomcatWebSocketHandlerAdapter extends WebSocketHandlerAdapterSupport { private TomcatWebSocketSession session; - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public TomcatWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler delegate) { - public TomcatWebSocketHandlerAdapter(WebSocketHandler delegate) { - Assert.notNull("WebSocketHandler is required"); - this.delegate = delegate; + super(request, response, delegate); } - @Override - public void onOpen(Session session, EndpointConfig config) { - this.session = new TomcatWebSocketSession(session); - - session.addMessageHandler(String.class, message -> { - WebSocketMessage webSocketMessage = toMessage(message); - this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); - }); - session.addMessageHandler(ByteBuffer.class, message -> { - WebSocketMessage webSocketMessage = toMessage(message); - this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); - }); - session.addMessageHandler(PongMessage.class, message -> { - WebSocketMessage webSocketMessage = toMessage(message); - this.session.handleMessage(webSocketMessage.getType(), webSocketMessage); - }); - - HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); - this.delegate.handle(this.session).subscribe(resultSubscriber); + public Endpoint getEndpoint() { + return new StandardEndpoint(); } - private WebSocketMessage toMessage(T message) { - if (message instanceof String) { - byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); - return WebSocketMessage.create(Type.TEXT, this.bufferFactory.wrap(bytes)); - } - else if (message instanceof ByteBuffer) { - DataBuffer buffer = this.bufferFactory.wrap((ByteBuffer) message); - return WebSocketMessage.create(Type.BINARY, buffer); - } - else if (message instanceof PongMessage) { - DataBuffer buffer = this.bufferFactory.wrap(((PongMessage) message).getApplicationData()); - return WebSocketMessage.create(Type.PONG, buffer); - } - else { - throw new IllegalArgumentException("Unexpected message type: " + message); - } + private TomcatWebSocketSession getSession() { + return this.session; } - @Override - public void onClose(Session session, CloseReason reason) { - if (this.session != null) { - int code = reason.getCloseCode().getCode(); - this.session.handleClose(new CloseStatus(code, reason.getReasonPhrase())); + + private class StandardEndpoint extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + TomcatWebSocketHandlerAdapter.this.session = new TomcatWebSocketSession(session); + + session.addMessageHandler(String.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + getSession().handleMessage(webSocketMessage.getType(), webSocketMessage); + }); + session.addMessageHandler(ByteBuffer.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + getSession().handleMessage(webSocketMessage.getType(), webSocketMessage); + }); + session.addMessageHandler(PongMessage.class, message -> { + WebSocketMessage webSocketMessage = toMessage(message); + getSession().handleMessage(webSocketMessage.getType(), webSocketMessage); + }); + + HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); + getDelegate().handle(TomcatWebSocketHandlerAdapter.this.session).subscribe(resultSubscriber); + } + + private WebSocketMessage toMessage(T message) { + if (message instanceof String) { + byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); + return WebSocketMessage.create(Type.TEXT, getBufferFactory().wrap(bytes)); + } + else if (message instanceof ByteBuffer) { + DataBuffer buffer = getBufferFactory().wrap((ByteBuffer) message); + return WebSocketMessage.create(Type.BINARY, buffer); + } + else if (message instanceof PongMessage) { + DataBuffer buffer = getBufferFactory().wrap(((PongMessage) message).getApplicationData()); + return WebSocketMessage.create(Type.PONG, buffer); + } + else { + throw new IllegalArgumentException("Unexpected message type: " + message); + } + } + + @Override + public void onClose(Session session, CloseReason reason) { + if (getSession() != null) { + int code = reason.getCloseCode().getCode(); + getSession().handleClose(new CloseStatus(code, reason.getReasonPhrase())); + } + } + + @Override + public void onError(Session session, Throwable exception) { + if (getSession() != null) { + getSession().handleError(exception); + } } } - @Override - public void onError(Session session, Throwable exception) { - if (this.session != null) { - this.session.handleError(exception); - } - } - - private final class HandlerResultSubscriber implements Subscriber { @Override @@ -127,15 +134,15 @@ public class TomcatWebSocketHandlerAdapter extends Endpoint { @Override public void onError(Throwable ex) { - if (session != null) { - session.close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); + if (getSession() != null) { + getSession().close(new CloseStatus(CloseStatus.SERVER_ERROR.getCode(), ex.getMessage())); } } @Override public void onComplete() { - if (session != null) { - session.close(); + if (getSession() != null) { + getSession().close(); } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java index a0a17c3716..17169806fb 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketHandlerAdapter.java @@ -16,22 +16,9 @@ package org.springframework.web.reactive.socket.adapter; -import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.util.Assert; -import org.springframework.web.reactive.socket.CloseStatus; -import org.springframework.web.reactive.socket.WebSocketHandler; -import org.springframework.web.reactive.socket.WebSocketMessage; -import org.springframework.web.reactive.socket.WebSocketMessage.Type; - import io.undertow.websockets.WebSocketConnectionCallback; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedBinaryMessage; @@ -39,6 +26,16 @@ import io.undertow.websockets.core.BufferedTextMessage; import io.undertow.websockets.core.CloseMessage; import io.undertow.websockets.core.WebSocketChannel; import io.undertow.websockets.spi.WebSocketHttpExchange; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.reactive.socket.CloseStatus; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketMessage.Type; /** * Undertow {@code WebSocketHandler} implementation adapting and @@ -47,36 +44,27 @@ import io.undertow.websockets.spi.WebSocketHttpExchange; * @author Violeta Georgieva * @since 5.0 */ -public class UndertowWebSocketHandlerAdapter implements WebSocketConnectionCallback { - - private final WebSocketHandler delegate; +public class UndertowWebSocketHandlerAdapter extends WebSocketHandlerAdapterSupport + implements WebSocketConnectionCallback { private UndertowWebSocketSession session; - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(false); + public UndertowWebSocketHandlerAdapter(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler delegate) { - public UndertowWebSocketHandlerAdapter(WebSocketHandler delegate) { - Assert.notNull("WebSocketHandler is required"); - this.delegate = delegate; + super(request, response, delegate); } @Override public void onConnect(WebSocketHttpExchange exchange, WebSocketChannel channel) { - try { - this.session = new UndertowWebSocketSession(channel); - } - catch (URISyntaxException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + this.session = new UndertowWebSocketSession(channel, getUri()); channel.getReceiveSetter().set(new UndertowReceiveListener()); channel.resumeReceives(); HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(); - this.delegate.handle(this.session).subscribe(resultSubscriber); + getDelegate().handle(this.session).subscribe(resultSubscriber); } @@ -114,14 +102,14 @@ public class UndertowWebSocketHandlerAdapter implements WebSocketConnectionCallb private WebSocketMessage toMessage(Type type, T message) { if (Type.TEXT.equals(type)) { byte[] bytes = ((String) message).getBytes(StandardCharsets.UTF_8); - return WebSocketMessage.create(Type.TEXT, bufferFactory.wrap(bytes)); + return WebSocketMessage.create(Type.TEXT, getBufferFactory().wrap(bytes)); } else if (Type.BINARY.equals(type)) { - DataBuffer buffer = bufferFactory.allocateBuffer().write((ByteBuffer[]) message); + DataBuffer buffer = getBufferFactory().allocateBuffer().write((ByteBuffer[]) message); return WebSocketMessage.create(Type.BINARY, buffer); } else if (Type.PONG.equals(type)) { - DataBuffer buffer = bufferFactory.allocateBuffer().write((ByteBuffer[]) message); + DataBuffer buffer = getBufferFactory().allocateBuffer().write((ByteBuffer[]) message); return WebSocketMessage.create(Type.PONG, buffer); } else { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java index 6d0b557753..298ccad696 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/UndertowWebSocketSession.java @@ -18,7 +18,6 @@ package org.springframework.web.reactive.socket.adapter; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -43,8 +42,8 @@ import org.springframework.web.reactive.socket.WebSocketSession; public class UndertowWebSocketSession extends AbstractListenerWebSocketSession { - public UndertowWebSocketSession(WebSocketChannel channel) throws URISyntaxException { - super(channel, ObjectUtils.getIdentityHexString(channel), new URI(channel.getUrl())); + public UndertowWebSocketSession(WebSocketChannel channel, URI url) { + super(channel, ObjectUtils.getIdentityHexString(channel), url); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/WebSocketHandlerAdapterSupport.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/WebSocketHandlerAdapterSupport.java index 9721850012..4a36ee2e0f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/WebSocketHandlerAdapterSupport.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/adapter/WebSocketHandlerAdapterSupport.java @@ -17,12 +17,15 @@ package org.springframework.web.reactive.socket.adapter; import java.net.URI; +import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.WebSocketHandler; /** - * Base class for {@link WebSocketHandler} implementations. + * Base class for {@link WebSocketHandler} adapters to underlying WebSocket + * handler APIs. * * @author Rossen Stoyanchev * @since 5.0 @@ -33,21 +36,32 @@ public abstract class WebSocketHandlerAdapterSupport { private final WebSocketHandler delegate; + private final DataBufferFactory bufferFactory; - protected WebSocketHandlerAdapterSupport(ServerHttpRequest request, WebSocketHandler handler) { - Assert.notNull("'request' is required"); - Assert.notNull("'handler' handler is required"); + + protected WebSocketHandlerAdapterSupport(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler handler) { + + Assert.notNull("ServerHttpRequest is required"); + Assert.notNull("ServerHttpResponse is required"); + Assert.notNull("WebSocketHandler handler is required"); this.uri = request.getURI(); + this.bufferFactory = response.bufferFactory(); this.delegate = handler; } - public URI getUri() { + protected URI getUri() { return this.uri; } - public WebSocketHandler getDelegate() { + protected WebSocketHandler getDelegate() { return this.delegate; } + @SuppressWarnings("unchecked") + protected T getBufferFactory() { + return (T) this.bufferFactory; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index 82295c6e0a..f898083b58 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -17,16 +17,14 @@ package org.springframework.web.reactive.socket.server.upgrade; import java.io.IOException; - import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.websocket.server.WebSocketServerFactory; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import reactor.core.publisher.Mono; + import org.springframework.context.Lifecycle; import org.springframework.core.NamedThreadLocal; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -39,8 +37,6 @@ import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdap import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; - /** * A {@link RequestUpgradeStrategy} for use with Jetty. * @@ -52,43 +48,13 @@ public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy, Life private static final ThreadLocal wsContainerHolder = new NamedThreadLocal<>("Jetty WebSocketHandler Adapter"); + private WebSocketServerFactory factory; private ServletContext servletContext; private volatile boolean running = false; - @Override - public Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler) { - - JettyWebSocketHandlerAdapter adapter = - new JettyWebSocketHandlerAdapter(webSocketHandler); - - HttpServletRequest servletRequest = getHttpServletRequest(exchange.getRequest()); - HttpServletResponse servletResponse = getHttpServletResponse(exchange.getResponse()); - - if (this.servletContext == null) { - this.servletContext = servletRequest.getServletContext(); - servletContext.setAttribute(DecoratedObjectFactory.ATTR, new DecoratedObjectFactory()); - } - - try { - start(); - - Assert.isTrue(this.factory.isUpgradeRequest(servletRequest, servletResponse), "Not a WebSocket handshake"); - - wsContainerHolder.set(adapter); - this.factory.acceptWebSocket(servletRequest, servletResponse); - } - catch (IOException ex) { - return Mono.error(ex); - } - finally { - wsContainerHolder.remove(); - } - - return Mono.empty(); - } @Override public void start() { @@ -96,16 +62,10 @@ public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy, Life this.running = true; try { this.factory = new WebSocketServerFactory(this.servletContext); - this.factory.setCreator(new WebSocketCreator() { - - @Override - public Object createWebSocket(ServletUpgradeRequest req, - ServletUpgradeResponse resp) { - JettyWebSocketHandlerAdapter adapter = wsContainerHolder.get(); - Assert.state(adapter != null, "Expected JettyWebSocketHandlerAdapter"); - return adapter; - } - + this.factory.setCreator((request, response) -> { + JettyWebSocketHandlerAdapter adapter = wsContainerHolder.get(); + Assert.state(adapter != null, "Expected JettyWebSocketHandlerAdapter"); + return adapter; }); this.factory.start(); } @@ -133,12 +93,46 @@ public class JettyRequestUpgradeStrategy implements RequestUpgradeStrategy, Life return this.running; } - private final HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { + @Override + public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler) { + + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter(request, response, handler); + + HttpServletRequest servletRequest = getHttpServletRequest(request); + HttpServletResponse servletResponse = getHttpServletResponse(response); + + if (this.servletContext == null) { + this.servletContext = servletRequest.getServletContext(); + this.servletContext.setAttribute(DecoratedObjectFactory.ATTR, new DecoratedObjectFactory()); + } + + try { + start(); + + Assert.isTrue(this.factory.isUpgradeRequest( + servletRequest, servletResponse), "Not a WebSocket handshake"); + + wsContainerHolder.set(adapter); + this.factory.acceptWebSocket(servletRequest, servletResponse); + } + catch (IOException ex) { + return Mono.error(ex); + } + finally { + wsContainerHolder.remove(); + } + + return Mono.empty(); + } + + private HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { Assert.isTrue(request instanceof ServletServerHttpRequest); return ((ServletServerHttpRequest) request).getServletRequest(); } - private final HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { + private HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { Assert.isTrue(response instanceof ServletServerHttpResponse); return ((ServletServerHttpResponse) response).getServletResponse(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java index 899973892c..dca9796551 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java @@ -24,6 +24,8 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.websocket.Endpoint; +import javax.websocket.server.ServerEndpointConfig; import org.apache.tomcat.websocket.server.WsServerContainer; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -50,45 +52,46 @@ public class TomcatRequestUpgradeStrategy implements RequestUpgradeStrategy { @Override - public Mono upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler){ + public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler){ - TomcatWebSocketHandlerAdapter endpoint = - new TomcatWebSocketHandlerAdapter(webSocketHandler); + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + Endpoint endpoint = new TomcatWebSocketHandlerAdapter(request, response, handler).getEndpoint(); - HttpServletRequest servletRequest = getHttpServletRequest(exchange.getRequest()); - HttpServletResponse servletResponse = getHttpServletResponse(exchange.getResponse()); + HttpServletRequest servletRequest = getHttpServletRequest(request); + HttpServletResponse servletResponse = getHttpServletResponse(response); - Map pathParams = Collections. emptyMap(); - - ServerEndpointRegistration sec = - new ServerEndpointRegistration(servletRequest.getRequestURI(), endpoint); + String requestURI = servletRequest.getRequestURI(); + ServerEndpointConfig config = new ServerEndpointRegistration(requestURI, endpoint); try { - getContainer(servletRequest).doUpgrade(servletRequest, servletResponse, - sec, pathParams); + WsServerContainer container = getContainer(servletRequest); + container.doUpgrade(servletRequest, servletResponse, config, Collections.emptyMap()); } - catch (ServletException | IOException e) { - return Mono.error(e); + catch (ServletException | IOException ex) { + return Mono.error(ex); } return Mono.empty(); } - private WsServerContainer getContainer(HttpServletRequest request) { - ServletContext servletContext = request.getServletContext(); - Object container = servletContext.getAttribute(SERVER_CONTAINER_ATTR); - Assert.notNull(container, "No '" + SERVER_CONTAINER_ATTR + "' ServletContext attribute. " + - "Are you running in a Servlet container that supports JSR-356?"); - Assert.isTrue(container instanceof WsServerContainer); - return (WsServerContainer) container; - } - - private final HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { + private HttpServletRequest getHttpServletRequest(ServerHttpRequest request) { Assert.isTrue(request instanceof ServletServerHttpRequest); return ((ServletServerHttpRequest) request).getServletRequest(); } - private final HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { + private HttpServletResponse getHttpServletResponse(ServerHttpResponse response) { Assert.isTrue(response instanceof ServletServerHttpResponse); return ((ServletServerHttpResponse) response).getServletResponse(); } + + private WsServerContainer getContainer(HttpServletRequest request) { + ServletContext servletContext = request.getServletContext(); + Object container = servletContext.getAttribute(SERVER_CONTAINER_ATTR); + Assert.notNull(container, + "No 'javax.websocket.server.ServerContainer' ServletContext attribute. " + + "Are you running in a Servlet container that supports JSR-356?"); + Assert.isTrue(container instanceof WsServerContainer); + return (WsServerContainer) container; + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java index 7cc22ddda7..ec13e2b685 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.socket.server.upgrade; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.UndertowServerHttpRequest; import org.springframework.util.Assert; import org.springframework.web.reactive.socket.WebSocketHandler; @@ -25,6 +26,7 @@ import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; import org.springframework.web.server.ServerWebExchange; import io.undertow.server.HttpServerExchange; +import io.undertow.websockets.WebSocketConnectionCallback; import io.undertow.websockets.WebSocketProtocolHandshakeHandler; import reactor.core.publisher.Mono; @@ -37,27 +39,23 @@ import reactor.core.publisher.Mono; public class UndertowRequestUpgradeStrategy implements RequestUpgradeStrategy { @Override - public Mono upgrade(ServerWebExchange exchange, - WebSocketHandler webSocketHandler) { + public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler) { - UndertowWebSocketHandlerAdapter callback = - new UndertowWebSocketHandlerAdapter(webSocketHandler); + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + WebSocketConnectionCallback callback = new UndertowWebSocketHandlerAdapter(request, response, handler); + + Assert.isTrue(request instanceof UndertowServerHttpRequest); + HttpServerExchange httpExchange = ((UndertowServerHttpRequest) request).getUndertowExchange(); - WebSocketProtocolHandshakeHandler handler = - new WebSocketProtocolHandshakeHandler(callback); try { - handler.handleRequest(getUndertowExchange(exchange.getRequest())); + new WebSocketProtocolHandshakeHandler(callback).handleRequest(httpExchange); } - catch (Exception e) { - return Mono.error(e); + catch (Exception ex) { + return Mono.error(ex); } return Mono.empty(); } - private final HttpServerExchange getUndertowExchange(ServerHttpRequest request) { - Assert.isTrue(request instanceof UndertowServerHttpRequest); - return ((UndertowServerHttpRequest) request).getUndertowExchange(); - } - }