Violeta Georgieva
8 years ago
committed by
Rossen Stoyanchev
7 changed files with 649 additions and 1 deletions
@ -0,0 +1,169 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
} |
Loading…
Reference in new issue