Initial reactive, WebSocket Tomcat support
Issue: SPR-14527
This commit is contained in:
parent
41ece612cf
commit
46b39f4372
|
|
@ -824,6 +824,11 @@ project("spring-web-reactive") {
|
||||||
}
|
}
|
||||||
optional("io.reactivex:rxjava:${rxjavaVersion}")
|
optional("io.reactivex:rxjava:${rxjavaVersion}")
|
||||||
optional("io.reactivex:rxjava-reactive-streams:${rxjavaAdapterVersion}")
|
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("io.projectreactor.addons:reactor-test:${reactorCoreVersion}")
|
||||||
testCompile("javax.validation:validation-api:${beanvalVersion}")
|
testCompile("javax.validation:validation-api:${beanvalVersion}")
|
||||||
testCompile("org.hibernate:hibernate-validator:${hibval5Version}")
|
testCompile("org.hibernate:hibernate-validator:${hibval5Version}")
|
||||||
|
|
|
||||||
|
|
@ -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<String>() {
|
||||||
|
|
||||||
|
@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<ByteBuffer>() {
|
||||||
|
|
||||||
|
@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<PongMessage>() {
|
||||||
|
|
||||||
|
@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 <T> 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<Void> {
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<Session> {
|
||||||
|
|
||||||
|
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<WebSocketMessage> receive() {
|
||||||
|
return Flux.from(this.webSocketMessagePublisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> send(Publisher<WebSocketMessage> 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<Void> 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<WebSocketMessage> {
|
||||||
|
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<WebSocketMessage> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>Class constructor accept a singleton {@link javax.websocket.Endpoint} instance.
|
||||||
|
*
|
||||||
|
* <p>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<Class<? extends Encoder>> getEncoders() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Class<? extends Decoder>> getDecoders() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> 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<String> getSubprotocols() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Extension> getExtensions() {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Configurator getConfigurator() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getEndpointInstance(Class<T> endpointClass)
|
||||||
|
throws InstantiationException {
|
||||||
|
return (T) getEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ServerEndpointRegistration for path '" + getPath() + "': " + getEndpointClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Void> upgrade(ServerWebExchange exchange, WebSocketHandler webSocketHandler){
|
||||||
|
|
||||||
|
TomcatWebSocketHandlerAdapter endpoint =
|
||||||
|
new TomcatWebSocketHandlerAdapter(webSocketHandler);
|
||||||
|
|
||||||
|
HttpServletRequest servletRequest = getHttpServletRequest(exchange.getRequest());
|
||||||
|
HttpServletResponse servletResponse = getHttpServletResponse(exchange.getResponse());
|
||||||
|
|
||||||
|
Map<String, String> pathParams = Collections.<String, String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,9 @@
|
||||||
*/
|
*/
|
||||||
package org.springframework.web.reactive.socket.server;
|
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.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.runner.RunWith;
|
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.HttpServer;
|
||||||
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
|
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
|
||||||
import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer;
|
import org.springframework.http.server.reactive.bootstrap.RxNettyHttpServer;
|
||||||
|
import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer;
|
||||||
import org.springframework.util.SocketUtils;
|
import org.springframework.util.SocketUtils;
|
||||||
import org.springframework.web.reactive.DispatcherHandler;
|
import org.springframework.web.reactive.DispatcherHandler;
|
||||||
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService;
|
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.support.WebSocketHandlerAdapter;
|
||||||
import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy;
|
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.RxNettyRequestUpgradeStrategy;
|
||||||
|
import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for WebSocket integration tests involving a server-side
|
* Base class for WebSocket integration tests involving a server-side
|
||||||
|
|
@ -59,9 +64,11 @@ public abstract class AbstractWebSocketHandlerIntegrationTests {
|
||||||
|
|
||||||
@Parameters
|
@Parameters
|
||||||
public static Object[][] arguments() {
|
public static Object[][] arguments() {
|
||||||
|
File base = new File(System.getProperty("java.io.tmpdir"));
|
||||||
return new Object[][] {
|
return new Object[][] {
|
||||||
{new ReactorHttpServer(), ReactorNettyConfig.class},
|
{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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
|
||||||
|
|
||||||
private String baseDir;
|
private String baseDir;
|
||||||
|
|
||||||
|
private Class<?> wsListener;
|
||||||
|
|
||||||
|
|
||||||
public TomcatHttpServer() {
|
public TomcatHttpServer() {
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +47,11 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
|
||||||
this.baseDir = baseDir;
|
this.baseDir = baseDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TomcatHttpServer(String baseDir, Class<?> wsListener) {
|
||||||
|
this.baseDir = baseDir;
|
||||||
|
this.wsListener = wsListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterPropertiesSet() throws Exception {
|
||||||
|
|
@ -61,6 +68,9 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
|
||||||
Context rootContext = tomcatServer.addContext("", base.getAbsolutePath());
|
Context rootContext = tomcatServer.addContext("", base.getAbsolutePath());
|
||||||
Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet);
|
Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet);
|
||||||
rootContext.addServletMappingDecoded("/", "httpHandlerServlet");
|
rootContext.addServletMappingDecoded("/", "httpHandlerServlet");
|
||||||
|
if (wsListener != null) {
|
||||||
|
rootContext.addApplicationListener(wsListener.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
|
private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue