Create spring-messaging module

Consolidates new, messaging-related classes from spring-context and
spring-websocket into one module.
This commit is contained in:
Rossen Stoyanchev
2013-07-12 09:02:51 -04:00
parent 2803845151
commit d3cecfc6cc
81 changed files with 404 additions and 315 deletions

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2002-2013 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.messaging;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public enum MessageType {
CONNECT,
MESSAGE,
SUBSCRIBE,
UNSUBSCRIBE,
DISCONNECT,
OTHER;
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2002-2012 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.messaging.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation indicating a method parameter should be bound to the body of a message.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageBody {
/**
* Whether body content is required.
* <p>Default is {@code true}, leading to an exception thrown in case
* there is no body content. Switch this to {@code false} if you prefer
* {@code null} to be passed when the body content is {@code null}.
*/
boolean required() default true;
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MessageExceptionHandler {
/**
* Exceptions handled by the annotation method. If empty, will default
* to any exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SubscribeEvent {
/**
* Destination value(s) for the subscription.
*/
String[] value() default {};
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UnsubscribeEvent {
/**
* Destination value(s) for the subscription.
*/
String[] value() default {};
}

View File

@@ -1,164 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractWebMessageHandler implements MessageHandler {
protected final Log logger = LogFactory.getLog(getClass());
private final List<String> allowedDestinations = new ArrayList<String>();
private final List<String> disallowedDestinations = new ArrayList<String>();
private final PathMatcher pathMatcher = new AntPathMatcher();
/**
* Ant-style destination patterns that this service is allowed to process.
*/
public void setAllowedDestinations(String... patterns) {
this.allowedDestinations.clear();
this.allowedDestinations.addAll(Arrays.asList(patterns));
}
/**
* Ant-style destination patterns that this service should skip.
*/
public void setDisallowedDestinations(String... patterns) {
this.disallowedDestinations.clear();
this.disallowedDestinations.addAll(Arrays.asList(patterns));
}
protected abstract Collection<MessageType> getSupportedMessageTypes();
protected boolean canHandle(Message<?> message, MessageType messageType) {
if (!CollectionUtils.isEmpty(getSupportedMessageTypes())) {
if (!getSupportedMessageTypes().contains(messageType)) {
return false;
}
}
return isDestinationAllowed(message);
}
protected boolean isDestinationAllowed(Message<?> message) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
String destination = headers.getDestination();
if (destination == null) {
return true;
}
if (!this.disallowedDestinations.isEmpty()) {
for (String pattern : this.disallowedDestinations) {
if (this.pathMatcher.match(pattern, destination)) {
if (logger.isTraceEnabled()) {
logger.trace("Skip message id=" + message.getHeaders().getId());
}
return false;
}
}
}
if (!this.allowedDestinations.isEmpty()) {
for (String pattern : this.allowedDestinations) {
if (this.pathMatcher.match(pattern, destination)) {
return true;
}
}
if (logger.isTraceEnabled()) {
logger.trace("Skip message id=" + message.getHeaders().getId());
}
return false;
}
return true;
}
@Override
public final void handleMessage(Message<?> message) throws MessagingException {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
MessageType messageType = headers.getMessageType();
if (!canHandle(message, messageType)) {
return;
}
if (MessageType.MESSAGE.equals(messageType)) {
handlePublish(message);
}
else if (MessageType.SUBSCRIBE.equals(messageType)) {
handleSubscribe(message);
}
else if (MessageType.UNSUBSCRIBE.equals(messageType)) {
handleUnsubscribe(message);
}
else if (MessageType.CONNECT.equals(messageType)) {
handleConnect(message);
}
else if (MessageType.DISCONNECT.equals(messageType)) {
handleDisconnect(message);
}
else {
handleOther(message);
}
}
protected void handleConnect(Message<?> message) {
}
protected void handlePublish(Message<?> message) {
}
protected void handleSubscribe(Message<?> message) {
}
protected void handleUnsubscribe(Message<?> message) {
}
protected void handleDisconnect(Message<?> message) {
}
protected void handleOther(Message<?> message) {
}
}

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.broker;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.util.MultiValueMap;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public abstract class AbstractSubscriptionRegistry implements SubscriptionRegistry {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public void addSubscription(Message<?> message) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
if (!MessageType.SUBSCRIBE.equals(headers.getMessageType())) {
logger.error("Expected SUBSCRIBE message: " + message);
return;
}
String sessionId = headers.getSessionId();
if (sessionId == null) {
logger.error("Ignoring subscription. No sessionId in message: " + message);
return;
}
String subscriptionId = headers.getSubscriptionId();
if (subscriptionId == null) {
logger.error("Ignoring subscription. No subscriptionId in message: " + message);
return;
}
String destination = headers.getDestination();
if (destination == null) {
logger.error("Ignoring destination. No destination in message: " + message);
return;
}
addSubscriptionInternal(sessionId, subscriptionId, destination, message);
}
protected abstract void addSubscriptionInternal(String sessionId, String subscriptionId,
String destination, Message<?> message);
@Override
public void removeSubscription(Message<?> message) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
if (!MessageType.UNSUBSCRIBE.equals(headers.getMessageType())) {
logger.error("Expected UNSUBSCRIBE message: " + message);
return;
}
String sessionId = headers.getSessionId();
if (sessionId == null) {
logger.error("Ignoring subscription. No sessionId in message: " + message);
return;
}
String subscriptionId = headers.getSubscriptionId();
if (subscriptionId == null) {
logger.error("Ignoring subscription. No subscriptionId in message: " + message);
return;
}
removeSubscriptionInternal(sessionId, subscriptionId, message);
}
protected abstract void removeSubscriptionInternal(String sessionId, String subscriptionId, Message<?> message);
@Override
public void removeSessionSubscriptions(String sessionId) {
}
@Override
public MultiValueMap<String, String> findSubscriptions(Message<?> message) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
if (!MessageType.MESSAGE.equals(headers.getMessageType())) {
logger.error("Unexpected message type: " + message);
return null;
}
String destination = headers.getDestination();
if (destination == null) {
logger.error("Ignoring destination. No destination in message: " + message);
return null;
}
return findSubscriptionsInternal(destination, message);
}
protected abstract MultiValueMap<String, String> findSubscriptionsInternal(
String destination, Message<?> message);
}

View File

@@ -1,240 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.broker;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.messaging.Message;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
private final DestinationCache destinationCache = new DestinationCache();
private final SessionSubscriptionRegistry subscriptionRegistry = new SessionSubscriptionRegistry();
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* @param pathMatcher the pathMatcher to set
*/
public void setPathMatcher(AntPathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
public AntPathMatcher getPathMatcher() {
return this.pathMatcher;
}
@Override
protected void addSubscriptionInternal(String sessionId, String subsId, String destination, Message<?> message) {
SessionSubscriptionInfo info = this.subscriptionRegistry.addSubscription(sessionId, subsId, destination);
if (!this.pathMatcher.isPattern(destination)) {
this.destinationCache.mapToDestination(destination, info);
}
}
@Override
protected void removeSubscriptionInternal(String sessionId, String subscriptionId, Message<?> message) {
SessionSubscriptionInfo info = this.subscriptionRegistry.getSubscriptions(sessionId);
if (info != null) {
String destination = info.removeSubscription(subscriptionId);
if (info.getSubscriptions(destination) == null) {
this.destinationCache.unmapFromDestination(destination, info);
}
}
}
@Override
public void removeSessionSubscriptions(String sessionId) {
SessionSubscriptionInfo info = this.subscriptionRegistry.removeSubscriptions(sessionId);
this.destinationCache.removeSessionSubscriptions(info);
}
@Override
protected MultiValueMap<String, String> findSubscriptionsInternal(String destination, Message<?> message) {
MultiValueMap<String,String> result = this.destinationCache.getSubscriptions(destination);
if (result.isEmpty()) {
result = new LinkedMultiValueMap<String, String>();
for (SessionSubscriptionInfo info : this.subscriptionRegistry.getAllSubscriptions()) {
for (String destinationPattern : info.getDestinations()) {
if (this.pathMatcher.match(destinationPattern, destination)) {
for (String subscriptionId : info.getSubscriptions(destinationPattern)) {
result.add(info.sessionId, subscriptionId);
}
}
}
}
}
return result;
}
/**
* Provide direct lookup of session subscriptions by destination (for non-pattern destinations).
*/
private static class DestinationCache {
// destination -> ..
private final Map<String, Set<SessionSubscriptionInfo>> subscriptionsByDestination =
new ConcurrentHashMap<String, Set<SessionSubscriptionInfo>>();
private final Object monitor = new Object();
public void mapToDestination(String destination, SessionSubscriptionInfo info) {
synchronized (monitor) {
Set<SessionSubscriptionInfo> registrations = this.subscriptionsByDestination.get(destination);
if (registrations == null) {
registrations = new CopyOnWriteArraySet<SessionSubscriptionInfo>();
this.subscriptionsByDestination.put(destination, registrations);
}
registrations.add(info);
}
}
public void unmapFromDestination(String destination, SessionSubscriptionInfo info) {
synchronized (monitor) {
Set<SessionSubscriptionInfo> infos = this.subscriptionsByDestination.get(destination);
if (infos != null) {
infos.remove(info);
if (infos.isEmpty()) {
this.subscriptionsByDestination.remove(destination);
}
}
}
}
public void removeSessionSubscriptions(SessionSubscriptionInfo info) {
for (String destination : info.getDestinations()) {
unmapFromDestination(destination, info);
}
}
public MultiValueMap<String, String> getSubscriptions(String destination) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>();
Set<SessionSubscriptionInfo> infos = this.subscriptionsByDestination.get(destination);
if (infos != null) {
for (SessionSubscriptionInfo info : infos) {
Set<String> subscriptions = info.getSubscriptions(destination);
if (subscriptions != null) {
for (String subscription : subscriptions) {
result.add(info.getSessionId(), subscription);
}
}
}
}
return result;
}
}
/**
* Provide access to session subscriptions by sessionId.
*/
private static class SessionSubscriptionRegistry {
private final Map<String, SessionSubscriptionInfo> sessions =
new ConcurrentHashMap<String, SessionSubscriptionInfo>();
public SessionSubscriptionInfo getSubscriptions(String sessionId) {
return this.sessions.get(sessionId);
}
public Collection<SessionSubscriptionInfo> getAllSubscriptions() {
return this.sessions.values();
}
public SessionSubscriptionInfo addSubscription(String sessionId, String subscriptionId, String destination) {
SessionSubscriptionInfo info = this.sessions.get(sessionId);
if (info == null) {
info = new SessionSubscriptionInfo(sessionId);
this.sessions.put(sessionId, info);
}
info.addSubscription(subscriptionId, destination);
return info;
}
public SessionSubscriptionInfo removeSubscriptions(String sessionId) {
return this.sessions.remove(sessionId);
}
}
/**
* Hold subscriptions for a session.
*/
private static class SessionSubscriptionInfo {
private final String sessionId;
private final Map<String, Set<String>> subscriptions = new HashMap<String, Set<String>>(4);
public SessionSubscriptionInfo(String sessionId) {
this.sessionId = sessionId;
}
public String getSessionId() {
return this.sessionId;
}
public Set<String> getDestinations() {
return this.subscriptions.keySet();
}
public Set<String> getSubscriptions(String destination) {
return this.subscriptions.get(destination);
}
public void addSubscription(String subscriptionId, String destination) {
Set<String> subs = this.subscriptions.get(destination);
if (subs == null) {
subs = new HashSet<String>(4);
this.subscriptions.put(destination, subs);
}
subs.add(subscriptionId);
}
public String removeSubscription(String subscriptionId) {
for (String destination : this.subscriptions.keySet()) {
Set<String> subscriptionIds = this.subscriptions.get(destination);
if (subscriptionIds.remove(subscriptionId)) {
if (subscriptionIds.isEmpty()) {
this.subscriptions.remove(destination);
}
return destination;
}
}
return null;
}
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.broker;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.service.AbstractWebMessageHandler;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class SimpleBrokerWebMessageHandler extends AbstractWebMessageHandler {
private final MessageChannel outboundChannel;
private SubscriptionRegistry subscriptionRegistry = new DefaultSubscriptionRegistry();
/**
* @param outboundChannel the channel to which messages for clients should be sent
* @param observable an Observable to use to manage subscriptions
*/
public SimpleBrokerWebMessageHandler(MessageChannel outboundChannel) {
Assert.notNull(outboundChannel, "outboundChannel is required");
this.outboundChannel = outboundChannel;
}
public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
Assert.notNull(subscriptionRegistry, "subscriptionRegistry is required");
this.subscriptionRegistry = subscriptionRegistry;
}
@Override
protected Collection<MessageType> getSupportedMessageTypes() {
return Arrays.asList(MessageType.MESSAGE, MessageType.SUBSCRIBE, MessageType.UNSUBSCRIBE);
}
@Override
public void handleSubscribe(Message<?> message) {
if (logger.isDebugEnabled()) {
logger.debug("Subscribe " + message);
}
this.subscriptionRegistry.addSubscription(message);
// TODO: need a way to communicate back if subscription was successfully created or
// not in which case an ERROR should be sent back and close the connection
// http://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE
}
@Override
protected void handleUnsubscribe(Message<?> message) {
this.subscriptionRegistry.removeSubscription(message);
}
@Override
public void handlePublish(Message<?> message) {
if (logger.isDebugEnabled()) {
logger.debug("Message received: " + message);
}
String destination = WebMessageHeaderAccesssor.wrap(message).getDestination();
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
for (String sessionId : subscriptions.keySet()) {
for (String subscriptionId : subscriptions.get(sessionId)) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
headers.setSessionId(sessionId);
headers.setSubscriptionId(subscriptionId);
Message<?> clientMessage = MessageBuilder.withPayload(
message.getPayload()).copyHeaders(headers.toMap()).build();
try {
this.outboundChannel.send(clientMessage);
}
catch (Throwable ex) {
logger.error("Failed to send message to destination=" + destination +
", sessionId=" + sessionId + ", subscriptionId=" + subscriptionId, ex);
}
}
}
}
@Override
public void handleDisconnect(Message<?> message) {
String sessionId = WebMessageHeaderAccesssor.wrap(message).getSessionId();
this.subscriptionRegistry.removeSessionSubscriptions(sessionId);
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.broker;
import org.springframework.messaging.Message;
import org.springframework.util.MultiValueMap;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface SubscriptionRegistry {
void addSubscription(Message<?> subscribeMessage);
void removeSubscription(Message<?> unsubscribeMessage);
void removeSessionSubscriptions(String sessionId);
MultiValueMap<String, String> findSubscriptions(Message<?> message);
}

View File

@@ -1,325 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.annotation.MessageMapping;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.annotation.SubscribeEvent;
import org.springframework.web.messaging.annotation.UnsubscribeEvent;
import org.springframework.web.messaging.service.AbstractWebMessageHandler;
import org.springframework.web.messaging.support.MessageHolder;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class AnnotationWebMessageHandler extends AbstractWebMessageHandler
implements ApplicationContextAware, InitializingBean {
private final MessageChannel inboundChannel;
private final MessageChannel outboundChannel;
private MessageConverter<?> messageConverter;
private ApplicationContext applicationContext;
private Map<MappingInfo, HandlerMethod> messageMethods = new HashMap<MappingInfo, HandlerMethod>();
private Map<MappingInfo, HandlerMethod> subscribeMethods = new HashMap<MappingInfo, HandlerMethod>();
private Map<MappingInfo, HandlerMethod> unsubscribeMethods = new HashMap<MappingInfo, HandlerMethod>();
private final Map<Class<?>, MessageExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<Class<?>, MessageExceptionHandlerMethodResolver>(64);
private ArgumentResolverComposite argumentResolvers = new ArgumentResolverComposite();
private ReturnValueHandlerComposite returnValueHandlers = new ReturnValueHandlerComposite();
/**
* @param inboundChannel a channel for processing incoming messages from clients
* @param outboundChannel a channel for messages going out to clients
*/
public AnnotationWebMessageHandler(MessageChannel inboundChannel, MessageChannel outboundChannel) {
Assert.notNull(inboundChannel, "inboundChannel is required");
Assert.notNull(outboundChannel, "outboundChannel is required");
this.inboundChannel = inboundChannel;
this.outboundChannel = outboundChannel;
}
/**
* TODO: multiple converters with 'content-type' header
*/
public void setMessageConverter(MessageConverter<?> converter) {
this.messageConverter = converter;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
protected Collection<MessageType> getSupportedMessageTypes() {
return Arrays.asList(MessageType.MESSAGE, MessageType.SUBSCRIBE, MessageType.UNSUBSCRIBE);
}
@Override
public void afterPropertiesSet() {
initHandlerMethods();
this.argumentResolvers.addResolver(new MessageBodyArgumentResolver(this.messageConverter));
this.returnValueHandlers.addHandler(
new MessageSendingReturnValueHandler(this.outboundChannel, this.messageConverter));
}
protected void initHandlerMethods() {
String[] beanNames = this.applicationContext.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
if (isHandler(this.applicationContext.getType(beanName))){
detectHandlerMethods(beanName);
}
}
}
protected boolean isHandler(Class<?> beanType) {
return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
(AnnotationUtils.findAnnotation(beanType, MessageMapping.class) != null));
}
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String) ?
this.applicationContext.getType((String) handler) : handler.getClass();
final Class<?> userType = ClassUtils.getUserClass(handlerType);
initHandlerMethods(handler, userType, MessageMapping.class,
new MessageMappingInfoCreator(), this.messageMethods);
initHandlerMethods(handler, userType, SubscribeEvent.class,
new SubscribeMappingInfoCreator(), this.subscribeMethods);
initHandlerMethods(handler, userType, UnsubscribeEvent.class,
new UnsubscribeMappingInfoCreator(), this.unsubscribeMethods);
}
private <A extends Annotation> void initHandlerMethods(Object handler, Class<?> handlerType,
final Class<A> annotationType, MappingInfoCreator<A> mappingInfoCreator,
Map<MappingInfo, HandlerMethod> handlerMethods) {
Set<Method> messageMethods = HandlerMethodSelector.selectMethods(handlerType, new MethodFilter() {
@Override
public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, annotationType) != null;
}
});
for (Method method : messageMethods) {
A annotation = AnnotationUtils.findAnnotation(method, annotationType);
HandlerMethod hm = createHandlerMethod(handler, method);
handlerMethods.put(mappingInfoCreator.create(annotation), hm);
}
}
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
handlerMethod = new HandlerMethod(beanName, this.applicationContext, method);
}
else {
handlerMethod = new HandlerMethod(handler, method);
}
return handlerMethod;
}
@Override
public void handlePublish(Message<?> message) {
handleMessageInternal(message, this.messageMethods);
}
@Override
public void handleSubscribe(Message<?> message) {
handleMessageInternal(message, this.subscribeMethods);
}
@Override
public void handleUnsubscribe(Message<?> message) {
handleMessageInternal(message, this.unsubscribeMethods);
}
private void handleMessageInternal(final Message<?> message, Map<MappingInfo, HandlerMethod> handlerMethods) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
String destination = headers.getDestination();
HandlerMethod match = getHandlerMethod(destination, handlerMethods);
if (match == null) {
return;
}
HandlerMethod handlerMethod = match.createWithResolvedBean();
// TODO: avoid re-creating invocableHandlerMethod
InvocableMessageHandlerMethod invocableHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod);
invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers);
try {
MessageHolder.setMessage(message);
Object value = invocableHandlerMethod.invoke(message);
MethodParameter returnType = handlerMethod.getReturnType();
if (void.class.equals(returnType.getParameterType())) {
return;
}
this.returnValueHandlers.handleReturnValue(value, returnType, message);
}
catch (Exception ex) {
invokeExceptionHandler(message, handlerMethod, ex);
}
catch (Throwable ex) {
// TODO
ex.printStackTrace();
}
finally {
MessageHolder.reset();
}
}
private void invokeExceptionHandler(Message<?> message, HandlerMethod handlerMethod, Exception ex) {
InvocableMessageHandlerMethod invocableHandlerMethod;
Class<?> beanType = handlerMethod.getBeanType();
MessageExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
if (resolver == null) {
resolver = new MessageExceptionHandlerMethodResolver(beanType);
this.exceptionHandlerCache.put(beanType, resolver);
}
Method method = resolver.resolveMethod(ex);
if (method == null) {
logger.error("Unhandled exception", ex);
return;
}
invocableHandlerMethod = new InvocableMessageHandlerMethod(handlerMethod.getBean(), method);
invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers);
try {
invocableHandlerMethod.invoke(message, ex);
}
catch (Throwable t) {
logger.error("Error while handling exception", t);
return;
}
}
protected HandlerMethod getHandlerMethod(String destination, Map<MappingInfo, HandlerMethod> handlerMethods) {
for (MappingInfo key : handlerMethods.keySet()) {
for (String mappingDestination : key.getDestinations()) {
if (destination.equals(mappingDestination)) {
return handlerMethods.get(key);
}
}
}
return null;
}
private static class MappingInfo {
private final List<String> destinations;
public MappingInfo(List<String> destinations) {
this.destinations = destinations;
}
public List<String> getDestinations() {
return this.destinations;
}
@Override
public String toString() {
return "MappingInfo [destinations=" + this.destinations + "]";
}
}
private interface MappingInfoCreator<A extends Annotation> {
MappingInfo create(A annotation);
}
private static class MessageMappingInfoCreator implements MappingInfoCreator<MessageMapping> {
@Override
public MappingInfo create(MessageMapping annotation) {
return new MappingInfo(Arrays.asList(annotation.value()));
}
}
private static class SubscribeMappingInfoCreator implements MappingInfoCreator<SubscribeEvent> {
@Override
public MappingInfo create(SubscribeEvent annotation) {
return new MappingInfo(Arrays.asList(annotation.value()));
}
}
private static class UnsubscribeMappingInfoCreator implements MappingInfoCreator<UnsubscribeEvent> {
@Override
public MappingInfo create(UnsubscribeEvent annotation) {
return new MappingInfo(Arrays.asList(annotation.value()));
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given message.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface ArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
*
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given message.
*
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to
* {@link #supportsParameter(org.springframework.core.MethodParameter)}
* and it must have returned {@code true}
* @param message
*
* @return the resolved argument value, or {@code null}.
*
* @throws Exception in case of errors with the preparation of argument values
*/
Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception;
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright 2002-2012 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.messaging.service.method;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Resolves method parameters by delegating to a list of registered
* {@link ArgumentResolver}. Previously resolved method parameters are cached
* for faster lookups.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ArgumentResolverComposite implements ArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
private final List<ArgumentResolver> argumentResolvers = new LinkedList<ArgumentResolver>();
private final Map<MethodParameter, ArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<MethodParameter, ArgumentResolver>(256);
/**
* Return a read-only list with the contained resolvers, or an empty list.
*/
public List<ArgumentResolver> getResolvers() {
return Collections.unmodifiableList(this.argumentResolvers);
}
/**
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
* {@link ArgumentResolver}.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
/**
* Iterate over registered {@link ArgumentResolver}s and invoke the one that supports it.
* @exception IllegalStateException if no suitable {@link ArgumentResolver} is found.
*/
@Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
ArgumentResolver resolver = getArgumentResolver(parameter);
Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
return resolver.resolveArgument(parameter, message);
}
/**
* Find a registered {@link ArgumentResolver} that supports the given method parameter.
*/
private ArgumentResolver getArgumentResolver(MethodParameter parameter) {
ArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (ArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
/**
* Add the given {@link ArgumentResolver}.
*/
public ArgumentResolverComposite addResolver(ArgumentResolver argumentResolver) {
this.argumentResolvers.add(argumentResolver);
return this;
}
/**
* Add the given {@link ArgumentResolver}s.
*/
public ArgumentResolverComposite addResolvers(List<? extends ArgumentResolver> argumentResolvers) {
if (argumentResolvers != null) {
for (ArgumentResolver resolver : argumentResolvers) {
this.argumentResolvers.add(resolver);
}
}
return this;
}
}

View File

@@ -1,246 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.messaging.Message;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.method.HandlerMethod;
/**
* Invokes the handler method for a given message after resolving
* its method argument values through registered {@link ArgumentResolver}s.
* <p>
* Argument resolution often requires a {@link WebDataBinder} for data binding or for type
* conversion. Use the {@link #setDataBinderFactory(WebDataBinderFactory)} property to
* supply a binder factory to pass to argument resolvers.
* <p>
* Use {@link #setMessageMethodArgumentResolvers(ArgumentResolverComposite)}
* to customize the list of argument resolvers.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class InvocableMessageHandlerMethod extends HandlerMethod {
private ArgumentResolverComposite argumentResolvers = new ArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
/**
* Create an instance from a {@code HandlerMethod}.
*/
public InvocableMessageHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
/**
* Create an instance from a bean instance and a method.
*/
public InvocableMessageHandlerMethod(Object bean, Method method) {
super(bean, method);
}
/**
* Constructs a new handler method with the given bean instance, method name and
* parameters.
*
* @param bean the object bean
* @param methodName the method name
* @param parameterTypes the method parameter types
* @throws NoSuchMethodException when the method cannot be found
*/
public InvocableMessageHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
throws NoSuchMethodException {
super(bean, methodName, parameterTypes);
}
/**
* Set {@link ArgumentResolver}s to use to use for resolving method
* argument values.
*/
public void setMessageMethodArgumentResolvers(ArgumentResolverComposite argumentResolvers) {
this.argumentResolvers = argumentResolvers;
}
/**
* Set the ParameterNameDiscoverer for resolving parameter names when needed (e.g.
* default request attribute name).
* <p>
* Default is an
* {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}
* instance.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* TODO
*
* @exception Exception raised if no suitable argument resolver can be found, or the
* method raised an exception
*/
public final Object invoke(Message<?> message, Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(message, providedArgs);
if (logger.isTraceEnabled()) {
StringBuilder builder = new StringBuilder("Invoking [");
builder.append(this.getMethod().getName()).append("] method with arguments ");
builder.append(Arrays.asList(args));
logger.trace(builder.toString());
}
Object returnValue = invoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + this.getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
/**
* Get the method argument values for the current request.
*/
private Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = this.argumentResolvers.resolveArgument(parameter, message);
continue;
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
}
throw ex;
}
}
if (args[i] == null) {
String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
throw new IllegalStateException(msg);
}
}
return args;
}
private String getArgumentResolutionErrorMessage(String message, int index) {
MethodParameter param = getMethodParameters()[index];
message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
return getDetailedErrorMessage(message);
}
/**
* Adds HandlerMethod details such as the controller type and method signature to the given error message.
* @param message error message to append the HandlerMethod details to
*/
protected String getDetailedErrorMessage(String message) {
StringBuilder sb = new StringBuilder(message).append("\n");
sb.append("HandlerMethod details: \n");
sb.append("Controller [").append(getBeanType().getName()).append("]\n");
sb.append("Method [").append(getBridgedMethod().toGenericString()).append("]\n");
return sb.toString();
}
/**
* Attempt to resolve a method parameter from the list of provided argument values.
*/
private Object resolveProvidedArgument(MethodParameter parameter, Object... providedArgs) {
if (providedArgs == null) {
return null;
}
for (Object providedArg : providedArgs) {
if (parameter.getParameterType().isInstance(providedArg)) {
return providedArg;
}
}
return null;
}
/**
* Invoke the handler method with the given argument values.
*/
private Object invoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException e) {
String msg = getInvocationErrorMessage(e.getMessage(), args);
throw new IllegalArgumentException(msg, e);
}
catch (InvocationTargetException e) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
String msg = getInvocationErrorMessage("Failed to invoke controller method", args);
throw new IllegalStateException(msg, targetException);
}
}
}
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
sb.append("Resolved arguments: \n");
for (int i=0; i < resolvedArgs.length; i++) {
sb.append("[").append(i).append("] ");
if (resolvedArgs[i] == null) {
sb.append("[null] \n");
}
else {
sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
sb.append("[value=").append(resolvedArgs[i]).append("]\n");
}
}
return sb.toString();
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.messaging.annotation.MessageBody;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class MessageBodyArgumentResolver implements ArgumentResolver {
private final MessageConverter<?> converter;
public MessageBodyArgumentResolver(MessageConverter<?> converter) {
Assert.notNull(converter, "converter is required");
this.converter = converter;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return true;
}
@Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
Object arg = null;
MessageBody annot = parameter.getParameterAnnotation(MessageBody.class);
if (annot == null || annot.required()) {
Class<?> sourceClass = message.getPayload().getClass();
Class<?> targetClass = parameter.getParameterType();
if (targetClass.isAssignableFrom(sourceClass)) {
return message.getPayload();
}
else {
// TODO: use content-type header
return this.converter.fromMessage(message, targetClass);
}
}
return arg;
}
}

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.messaging.annotation.MessageExceptionHandler;
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class MessageExceptionHandlerMethodResolver extends ExceptionHandlerMethodResolver {
public MessageExceptionHandlerMethodResolver(Class<?> handlerType) {
super(handlerType);
}
@Override
protected MethodFilter getExceptionHandlerMethods() {
return MESSAGE_EXCEPTION_HANDLER_METHODS;
}
@Override
protected void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
MessageExceptionHandler annotation = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class);
result.addAll(Arrays.asList(annotation.value()));
}
/**
* A filter for selecting {@code @ExceptionHandler} methods.
*/
public final static MethodFilter MESSAGE_EXCEPTION_HANDLER_METHODS = new MethodFilter() {
@Override
public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class) != null;
}
};
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class MessageSendingReturnValueHandler implements ReturnValueHandler {
private MessageChannel outboundChannel;
private final MessageConverter converter;
public MessageSendingReturnValueHandler(MessageChannel outboundChannel, MessageConverter<?> converter) {
Assert.notNull(outboundChannel, "outboundChannel is required");
Assert.notNull(converter, "converter is required");
this.outboundChannel = outboundChannel;
this.converter = converter;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return true;
}
@SuppressWarnings("unchecked")
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message)
throws Exception {
if (returnValue == null) {
return;
}
WebMessageHeaderAccesssor inputHeaders = WebMessageHeaderAccesssor.wrap(message);
Message<?> returnMessage = (returnValue instanceof Message) ? (Message<?>) returnValue : null;
Object returnPayload = (returnMessage != null) ? returnMessage.getPayload() : returnValue;
WebMessageHeaderAccesssor returnHeaders = (returnMessage != null) ?
WebMessageHeaderAccesssor.wrap(returnMessage) : WebMessageHeaderAccesssor.create();
returnHeaders.setSessionId(inputHeaders.getSessionId());
returnHeaders.setSubscriptionId(inputHeaders.getSubscriptionId());
if (returnHeaders.getDestination() == null) {
returnHeaders.setDestination(inputHeaders.getDestination());
}
returnMessage = this.converter.toMessage(returnPayload);
returnMessage = MessageBuilder.fromMessage(returnMessage).copyHeaders(returnHeaders.toMap()).build();
this.outboundChannel.send(returnMessage);
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
/**
* Strategy interface to handle the value returned from the invocation of a
* handler method .
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface ReturnValueHandler {
/**
* Whether the given {@linkplain MethodParameter method return type} is
* supported by this handler.
*
* @param returnType the method return type to check
* @return {@code true} if this handler supports the supplied return type;
* {@code false} otherwise
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* Handle the given return value.
*
* @param returnValue the value returned from the handler method
* @param returnType the type of the return value. This type must have
* previously been passed to
* {@link #supportsReturnType(org.springframework.core.MethodParameter)}
* and it must have returned {@code true}
* @param message the message that caused this method to be called
* @throws Exception if the return value handling results in an error
*/
void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message) throws Exception;
}

View File

@@ -1,79 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ReturnValueHandlerComposite implements ReturnValueHandler {
private final List<ReturnValueHandler> returnValueHandlers = new ArrayList<ReturnValueHandler>();
/**
* Add the given {@link ReturnValueHandler}.
*/
public ReturnValueHandlerComposite addHandler(ReturnValueHandler returnValuehandler) {
this.returnValueHandlers.add(returnValuehandler);
return this;
}
/**
* Add the given {@link ReturnValueHandler}s.
*/
public ReturnValueHandlerComposite addHandlers(List<? extends ReturnValueHandler> handlers) {
if (handlers != null) {
for (ReturnValueHandler handler : handlers) {
this.returnValueHandlers.add(handler);
}
}
return this;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return getReturnValueHandler(returnType) != null;
}
private ReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
for (ReturnValueHandler handler : this.returnValueHandlers) {
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message)
throws Exception {
ReturnValueHandler handler = getReturnValueHandler(returnType);
Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]");
handler.handleReturnValue(returnValue, returnType, message);
}
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.messaging.MessageType;
/**
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public enum StompCommand {
// client
CONNECT,
STOMP,
SEND,
SUBSCRIBE,
UNSUBSCRIBE,
ACK,
NACK,
BEGIN,
COMMIT,
ABORT,
DISCONNECT,
// server
CONNECTED,
MESSAGE,
RECEIPT,
ERROR;
private static Map<StompCommand, MessageType> commandToMessageType = new HashMap<StompCommand, MessageType>();
static {
commandToMessageType.put(StompCommand.CONNECT, MessageType.CONNECT);
commandToMessageType.put(StompCommand.STOMP, MessageType.CONNECT);
commandToMessageType.put(StompCommand.SEND, MessageType.MESSAGE);
commandToMessageType.put(StompCommand.MESSAGE, MessageType.MESSAGE);
commandToMessageType.put(StompCommand.SUBSCRIBE, MessageType.SUBSCRIBE);
commandToMessageType.put(StompCommand.UNSUBSCRIBE, MessageType.UNSUBSCRIBE);
commandToMessageType.put(StompCommand.DISCONNECT, MessageType.DISCONNECT);
}
public MessageType getMessageType() {
MessageType messageType = commandToMessageType.get(this);
return (messageType != null) ? messageType : MessageType.OTHER;
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp;
import org.springframework.core.NestedRuntimeException;
/**
* @author Gary Russell
* @since 4.0
*/
@SuppressWarnings("serial")
public class StompConversionException extends NestedRuntimeException {
public StompConversionException(String msg, Throwable cause) {
super(msg, cause);
}
public StompConversionException(String msg) {
super(msg);
}
}

View File

@@ -1,312 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp.support;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.messaging.stomp.StompCommand;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
/**
* Can be used to prepare headers for a new STOMP message, or to access and/or modify
* STOMP-specific headers of an existing message.
* <p>
* Use one of the static factory method in this class, then call getters and setters, and
* at the end if necessary call {@link #toMap()} to obtain the updated headers
* or call {@link #toNativeHeaderMap()} to obtain only the STOMP-specific headers.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class StompHeaderAccessor extends WebMessageHeaderAccesssor {
public static final String STOMP_ID = "id";
public static final String HOST = "host";
public static final String ACCEPT_VERSION = "accept-version";
public static final String MESSAGE_ID = "message-id";
public static final String RECEIPT_ID = "receipt-id";
public static final String SUBSCRIPTION = "subscription";
public static final String VERSION = "version";
public static final String MESSAGE = "message";
public static final String ACK = "ack";
public static final String NACK = "nack";
public static final String LOGIN = "login";
public static final String PASSCODE = "passcode";
public static final String DESTINATION = "destination";
public static final String CONTENT_TYPE = "content-type";
public static final String CONTENT_LENGTH = "content-length";
public static final String HEARTBEAT = "heart-beat";
private static final AtomicLong messageIdCounter = new AtomicLong();
/**
* A constructor for creating new STOMP message headers.
*/
private StompHeaderAccessor(StompCommand command, Map<String, List<String>> externalSourceHeaders) {
super(command.getMessageType(), command, externalSourceHeaders);
initWebMessageHeaders();
}
private void initWebMessageHeaders() {
String destination = getFirstNativeHeader(DESTINATION);
if (destination != null) {
super.setDestination(destination);
}
String contentType = getFirstNativeHeader(CONTENT_TYPE);
if (contentType != null) {
super.setContentType(MediaType.parseMediaType(contentType));
}
if (StompCommand.SUBSCRIBE.equals(getStompCommand()) || StompCommand.UNSUBSCRIBE.equals(getStompCommand())) {
if (getFirstNativeHeader(STOMP_ID) != null) {
super.setSubscriptionId(getFirstNativeHeader(STOMP_ID));
}
}
}
/**
* A constructor for accessing and modifying existing message headers.
*/
private StompHeaderAccessor(Message<?> message) {
super(message);
}
/**
* Create {@link StompHeaderAccessor} for a new {@link Message}.
*/
public static StompHeaderAccessor create(StompCommand command) {
return new StompHeaderAccessor(command, null);
}
/**
* Create {@link StompHeaderAccessor} from parsed STOP frame content.
*/
public static StompHeaderAccessor create(StompCommand command, Map<String, List<String>> headers) {
return new StompHeaderAccessor(command, headers);
}
/**
* Create {@link StompHeaderAccessor} from the headers of an existing {@link Message}.
*/
public static StompHeaderAccessor wrap(Message<?> message) {
return new StompHeaderAccessor(message);
}
/**
* Return STOMP headers including original, wrapped STOMP headers (if any) plus
* additional header updates made through accessor methods.
*/
@Override
public Map<String, List<String>> toNativeHeaderMap() {
Map<String, List<String>> result = super.toNativeHeaderMap();
String destination = super.getDestination();
if (destination != null) {
result.put(DESTINATION, Arrays.asList(destination));
}
MediaType contentType = getContentType();
if (contentType != null) {
result.put(CONTENT_TYPE, Arrays.asList(contentType.toString()));
}
if (StompCommand.MESSAGE.equals(getStompCommand())) {
String subscriptionId = getSubscriptionId();
if (subscriptionId != null) {
result.put(SUBSCRIPTION, Arrays.asList(subscriptionId));
}
else {
logger.warn("STOMP MESSAGE frame should have a subscription: " + this.toString());
}
if ((getMessageId() == null)) {
String messageId = getSessionId() + "-" + messageIdCounter.getAndIncrement();
result.put(MESSAGE_ID, Arrays.asList(messageId));
}
}
return result;
}
public void setStompCommandIfNotSet(StompCommand command) {
if (getStompCommand() == null) {
setProtocolMessageType(command);
}
}
public StompCommand getStompCommand() {
return (StompCommand) super.getProtocolMessageType();
}
public Set<String> getAcceptVersion() {
String rawValue = getFirstNativeHeader(ACCEPT_VERSION);
return (rawValue != null) ? StringUtils.commaDelimitedListToSet(rawValue) : Collections.<String>emptySet();
}
public void setAcceptVersion(String acceptVersion) {
setNativeHeader(ACCEPT_VERSION, acceptVersion);
}
public void setHost(String host) {
setNativeHeader(HOST, host);
}
public String getHost() {
return getFirstNativeHeader(HOST);
}
@Override
public void setDestination(String destination) {
super.setDestination(destination);
setNativeHeader(DESTINATION, destination);
}
@Override
public void setDestinations(List<String> destinations) {
Assert.isTrue((destinations != null) && (destinations.size() == 1), "STOMP allows one destination per message");
super.setDestinations(destinations);
setNativeHeader(DESTINATION, destinations.get(0));
}
public long[] getHeartbeat() {
String rawValue = getFirstNativeHeader(HEARTBEAT);
if (!StringUtils.hasText(rawValue)) {
return null;
}
String[] rawValues = StringUtils.commaDelimitedListToStringArray(rawValue);
// TODO assertions
return new long[] { Long.valueOf(rawValues[0]), Long.valueOf(rawValues[1])};
}
public void setContentType(MediaType mediaType) {
if (mediaType != null) {
super.setContentType(mediaType);
setNativeHeader(CONTENT_TYPE, mediaType.toString());
}
}
public MediaType getContentType() {
String value = getFirstNativeHeader(CONTENT_TYPE);
return (value != null) ? MediaType.parseMediaType(value) : null;
}
public Integer getContentLength() {
String contentLength = getFirstNativeHeader(CONTENT_LENGTH);
return StringUtils.hasText(contentLength) ? new Integer(contentLength) : null;
}
public void setContentLength(int contentLength) {
setNativeHeader(CONTENT_LENGTH, String.valueOf(contentLength));
}
public void setHeartbeat(long cx, long cy) {
setNativeHeader(HEARTBEAT, StringUtils.arrayToCommaDelimitedString(new Object[] {cx, cy}));
}
public void setAck(String ack) {
setNativeHeader(ACK, ack);
}
public String getAck() {
return getFirstNativeHeader(ACK);
}
public void setNack(String nack) {
setNativeHeader(NACK, nack);
}
public String getNack() {
return getFirstNativeHeader(NACK);
}
public void setLogin(String login) {
setNativeHeader(LOGIN, login);
}
public String getLogin() {
return getFirstNativeHeader(LOGIN);
}
public void setPasscode(String passcode) {
setNativeHeader(PASSCODE, passcode);
}
public String getPasscode() {
return getFirstNativeHeader(PASSCODE);
}
public void setReceiptId(String receiptId) {
setNativeHeader(RECEIPT_ID, receiptId);
}
public String getReceiptId() {
return getFirstNativeHeader(RECEIPT_ID);
}
public String getMessage() {
return getFirstNativeHeader(MESSAGE);
}
public void setMessage(String content) {
setNativeHeader(MESSAGE, content);
}
public String getMessageId() {
return getFirstNativeHeader(MESSAGE_ID);
}
public void setMessageId(String id) {
setNativeHeader(MESSAGE_ID, id);
}
public String getVersion() {
return getFirstNativeHeader(VERSION);
}
public void setVersion(String version) {
setNativeHeader(VERSION, version);
}
}

View File

@@ -1,237 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp.support;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map.Entry;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.messaging.stomp.StompCommand;
import org.springframework.web.messaging.stomp.StompConversionException;
/**
* @author Gary Russell
* @author Rossen Stoyanchev
* @since 4.0
*/
public class StompMessageConverter {
private static final Charset STOMP_CHARSET = Charset.forName("UTF-8");
public static final byte LF = 0x0a;
public static final byte CR = 0x0d;
private static final byte COLON = ':';
/**
* @param stompContent a complete STOMP message (without the trailing 0x00) as byte[] or String.
*/
public Message<?> toMessage(Object stompContent, String sessionId) {
byte[] byteContent = null;
if (stompContent instanceof String) {
byteContent = ((String) stompContent).getBytes(STOMP_CHARSET);
}
else if (stompContent instanceof byte[]){
byteContent = (byte[]) stompContent;
}
else {
throw new IllegalArgumentException(
"stompContent is neither String nor byte[]: " + stompContent.getClass());
}
int totalLength = byteContent.length;
if (byteContent[totalLength-1] == 0) {
totalLength--;
}
int payloadIndex = findIndexOfPayload(byteContent);
if (payloadIndex == 0) {
throw new StompConversionException("No command found");
}
String headerContent = new String(byteContent, 0, payloadIndex, STOMP_CHARSET);
Parser parser = new Parser(headerContent);
// TODO: validate command and whether a payload is allowed
StompCommand command = StompCommand.valueOf(parser.nextToken(LF).trim());
Assert.notNull(command, "No command found");
MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
while (parser.hasNext()) {
String header = parser.nextToken(COLON);
if (header != null) {
if (parser.hasNext()) {
String value = parser.nextToken(LF);
headers.add(header, value);
}
else {
throw new StompConversionException("Parse exception for " + headerContent);
}
}
}
StompHeaderAccessor stompHeaders = StompHeaderAccessor.create(command, headers);
stompHeaders.setSessionId(sessionId);
byte[] payload = new byte[totalLength - payloadIndex];
System.arraycopy(byteContent, payloadIndex, payload, 0, totalLength - payloadIndex);
return MessageBuilder.withPayload(payload).copyHeaders(stompHeaders.toMap()).build();
}
private int findIndexOfPayload(byte[] bytes) {
int i;
// ignore any leading EOL from the previous message
for (i = 0; i < bytes.length; i++) {
if (bytes[i] != '\n' && bytes[i] != '\r') {
break;
}
bytes[i] = ' ';
}
int index = 0;
for (; i < bytes.length - 1; i++) {
if (bytes[i] == LF && bytes[i+1] == LF) {
index = i + 2;
break;
}
if ((i < (bytes.length - 3)) &&
(bytes[i] == CR && bytes[i+1] == LF && bytes[i+2] == CR && bytes[i+3] == LF)) {
index = i + 4;
break;
}
}
if (i >= bytes.length) {
throw new StompConversionException("No end of headers found");
}
return index;
}
public byte[] fromMessage(Message<?> message) {
byte[] payload;
if (message.getPayload() instanceof byte[]) {
payload = (byte[]) message.getPayload();
}
else {
throw new IllegalArgumentException(
"stompContent is not byte[]: " + message.getPayload().getClass());
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
try {
out.write(stompHeaders.getStompCommand().toString().getBytes("UTF-8"));
out.write(LF);
for (Entry<String, List<String>> entry : stompHeaders.toNativeHeaderMap().entrySet()) {
String key = entry.getKey();
key = replaceAllOutbound(key);
for (String value : entry.getValue()) {
out.write(key.getBytes("UTF-8"));
out.write(COLON);
value = replaceAllOutbound(value);
out.write(value.getBytes("UTF-8"));
out.write(LF);
}
}
out.write(LF);
out.write(payload);
out.write(0);
return out.toByteArray();
}
catch (IOException e) {
throw new StompConversionException("Failed to serialize " + message, e);
}
}
private String replaceAllOutbound(String key) {
return key.replaceAll("\\\\", "\\\\")
.replaceAll(":", "\\\\c")
.replaceAll("\n", "\\\\n")
.replaceAll("\r", "\\\\r");
}
private class Parser {
private final String content;
private int offset;
public Parser(String content) {
this.content = content;
}
public boolean hasNext() {
return this.offset < this.content.length();
}
public String nextToken(byte delimiter) {
if (this.offset >= this.content.length()) {
return null;
}
int delimAt = this.content.indexOf(delimiter, this.offset);
if (delimAt == -1) {
if (this.offset == this.content.length() - 1 && delimiter == COLON &&
this.content.charAt(this.offset) == LF) {
this.offset++;
return null;
}
else if (this.offset == this.content.length() - 2 && delimiter == COLON &&
this.content.charAt(this.offset) == CR &&
this.content.charAt(this.offset + 1) == LF) {
this.offset += 2;
return null;
}
else {
throw new StompConversionException("No delimiter found at offset " + offset + " in " + this.content);
}
}
int escapeAt = this.content.indexOf('\\', this.offset);
String token = this.content.substring(this.offset, delimAt + 1);
this.offset += token.length();
if (escapeAt >= 0 && escapeAt < delimAt) {
char escaped = this.content.charAt(escapeAt + 1);
if (escaped == 'n' || escaped == 'c' || escaped == '\\') {
token = token.replaceAll("\\\\n", "\n")
.replaceAll("\\\\r", "\r")
.replaceAll("\\\\c", ":")
.replaceAll("\\\\\\\\", "\\\\");
}
else {
throw new StompConversionException("Invalid escape sequence \\" + escaped);
}
}
int length = token.length();
if (delimiter == LF && length > 1 && token.charAt(length - 2) == CR) {
return token.substring(0, length - 2);
}
else {
return token.substring(0, length - 1);
}
}
}
}

View File

@@ -1,445 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp.support;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.springframework.context.SmartLifecycle;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.service.AbstractWebMessageHandler;
import org.springframework.web.messaging.stomp.StompCommand;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import reactor.core.Environment;
import reactor.core.composable.Promise;
import reactor.function.Consumer;
import reactor.tcp.TcpClient;
import reactor.tcp.TcpConnection;
import reactor.tcp.encoding.DelimitedCodec;
import reactor.tcp.encoding.StandardCodecs;
import reactor.tcp.netty.NettyTcpClient;
import reactor.tcp.spec.TcpClientSpec;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class StompRelayWebMessageHandler extends AbstractWebMessageHandler implements SmartLifecycle {
private static final String STOMP_RELAY_SYSTEM_SESSION_ID = "stompRelaySystemSessionId";
private MessageChannel outboundChannel;
private String relayHost = "127.0.0.1";
private int relayPort = 61613;
private String systemLogin = "guest";
private String systemPasscode = "guest";
private final StompMessageConverter stompMessageConverter = new StompMessageConverter();
private Environment environment;
private TcpClient<String, String> tcpClient;
private final Map<String, RelaySession> relaySessions = new ConcurrentHashMap<String, RelaySession>();
private Object lifecycleMonitor = new Object();
private boolean running = false;
/**
* @param outboundChannel a channel for messages going out to clients
*/
public StompRelayWebMessageHandler(MessageChannel outboundChannel) {
Assert.notNull(outboundChannel, "outboundChannel is required");
this.outboundChannel = outboundChannel;
}
/**
* Set the STOMP message broker host.
*/
public void setRelayHost(String relayHost) {
Assert.hasText(relayHost, "relayHost must not be empty");
this.relayHost = relayHost;
}
/**
* @return the STOMP message broker host.
*/
public String getRelayHost() {
return this.relayHost;
}
/**
* Set the STOMP message broker port.
*/
public void setRelayPort(int relayPort) {
this.relayPort = relayPort;
}
/**
* @return the STOMP message broker port.
*/
public int getRelayPort() {
return this.relayPort;
}
/**
* Set the login for a "system" TCP connection used to send messages to the STOMP
* broker without having a client session (e.g. REST/HTTP request handling method).
*/
public void setSystemLogin(String systemLogin) {
Assert.hasText(systemLogin, "systemLogin must not be empty");
this.systemLogin = systemLogin;
}
/**
* @return the login for a shared, "system" connection to the STOMP message broker.
*/
public String getSystemLogin() {
return this.systemLogin;
}
/**
* Set the passcode for a "system" TCP connection used to send messages to the STOMP
* broker without having a client session (e.g. REST/HTTP request handling method).
*/
public void setSystemPasscode(String systemPasscode) {
this.systemPasscode = systemPasscode;
}
/**
* @return the passcode for a shared, "system" connection to the STOMP message broker.
*/
public String getSystemPasscode() {
return this.systemPasscode;
}
@Override
protected Collection<MessageType> getSupportedMessageTypes() {
return null;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public int getPhase() {
return Integer.MAX_VALUE;
}
@Override
public boolean isRunning() {
synchronized (this.lifecycleMonitor) {
return this.running;
}
}
@Override
public void start() {
synchronized (this.lifecycleMonitor) {
this.environment = new Environment();
this.tcpClient = new TcpClientSpec<String, String>(NettyTcpClient.class)
.env(this.environment)
.codec(new DelimitedCodec<String, String>((byte) 0, true, StandardCodecs.STRING_CODEC))
.connect(this.relayHost, this.relayPort)
.get();
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
headers.setAcceptVersion("1.1,1.2");
headers.setLogin(this.systemLogin);
headers.setPasscode(this.systemPasscode);
headers.setHeartbeat(0,0); // TODO
Message<?> message = MessageBuilder.withPayload(
new byte[0]).copyHeaders(headers.toNativeHeaderMap()).build();
RelaySession session = new RelaySession(message, headers) {
@Override
protected void sendMessageToClient(Message<?> message) {
// TODO: check for ERROR frame (reconnect?)
}
};
this.relaySessions.put(STOMP_RELAY_SYSTEM_SESSION_ID, session);
this.running = true;
}
}
@Override
public void stop() {
synchronized (this.lifecycleMonitor) {
this.running = false;
try {
this.tcpClient.close().await(5000, TimeUnit.MILLISECONDS);
this.environment.shutdown();
}
catch (InterruptedException e) {
// ignore
}
}
}
@Override
public void stop(Runnable callback) {
synchronized (this.lifecycleMonitor) {
stop();
callback.run();
}
}
@Override
public void handleConnect(Message<?> message) {
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
String sessionId = stompHeaders.getSessionId();
if (sessionId == null) {
logger.error("No sessionId in message " + message);
return;
}
RelaySession relaySession = new RelaySession(message, stompHeaders);
this.relaySessions.put(sessionId, relaySession);
}
@Override
public void handlePublish(Message<?> message) {
forwardMessage(message, StompCommand.SEND);
}
@Override
public void handleSubscribe(Message<?> message) {
forwardMessage(message, StompCommand.SUBSCRIBE);
}
@Override
public void handleUnsubscribe(Message<?> message) {
forwardMessage(message, StompCommand.UNSUBSCRIBE);
}
@Override
public void handleDisconnect(Message<?> message) {
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
if (stompHeaders.getStompCommand() != null) {
forwardMessage(message, StompCommand.DISCONNECT);
}
String sessionId = stompHeaders.getSessionId();
if (sessionId == null) {
logger.error("No sessionId in message " + message);
return;
}
}
@Override
public void handleOther(Message<?> message) {
StompCommand command = (StompCommand) message.getHeaders().get(WebMessageHeaderAccesssor.PROTOCOL_MESSAGE_TYPE);
Assert.notNull(command, "Expected STOMP command: " + message.getHeaders());
forwardMessage(message, command);
}
private void forwardMessage(Message<?> message, StompCommand command) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
headers.setStompCommandIfNotSet(command);
String sessionId = headers.getSessionId();
if (sessionId == null) {
if (StompCommand.SEND.equals(command)) {
sessionId = STOMP_RELAY_SYSTEM_SESSION_ID;
}
else {
logger.error("No sessionId in message " + message);
return;
}
}
RelaySession session = this.relaySessions.get(sessionId);
if (session == null) {
logger.warn("Session id=" + sessionId + " not found. Message cannot be forwarded: " + message);
return;
}
session.forward(message, headers);
}
private class RelaySession {
private final String sessionId;
private final Promise<TcpConnection<String, String>> promise;
private final BlockingQueue<Message<?>> messageQueue = new LinkedBlockingQueue<Message<?>>(50);
private final Object monitor = new Object();
private volatile boolean isConnected = false;
public RelaySession(final Message<?> message, final StompHeaderAccessor stompHeaders) {
Assert.notNull(message, "message is required");
Assert.notNull(stompHeaders, "stompHeaders is required");
this.sessionId = stompHeaders.getSessionId();
this.promise = tcpClient.open();
this.promise.consume(new Consumer<TcpConnection<String,String>>() {
@Override
public void accept(TcpConnection<String, String> connection) {
connection.in().consume(new Consumer<String>() {
@Override
public void accept(String stompFrame) {
readStompFrame(stompFrame);
}
});
stompHeaders.setHeartbeat(0,0); // TODO
forwardInternal(message, stompHeaders, connection);
}
});
this.promise.onError(new Consumer<Throwable>() {
@Override
public void accept(Throwable ex) {
relaySessions.remove(sessionId);
logger.error("Failed to connect to broker", ex);
sendError(sessionId, "Failed to connect to message broker " + ex.toString());
}
});
// TODO: ATM no way to detect closed socket
}
private void readStompFrame(String stompFrame) {
if (StringUtils.isEmpty(stompFrame)) {
// heartbeat?
return;
}
Message<?> message = stompMessageConverter.toMessage(stompFrame, this.sessionId);
if (logger.isTraceEnabled()) {
logger.trace("Reading message " + message);
}
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECTED == headers.getStompCommand()) {
synchronized(this.monitor) {
this.isConnected = true;
flushMessages(promise.get());
}
return;
}
if (StompCommand.ERROR == headers.getStompCommand()) {
if (logger.isDebugEnabled()) {
logger.warn("STOMP ERROR: " + headers.getMessage() + ". Removing session: " + this.sessionId);
}
relaySessions.remove(this.sessionId);
}
sendMessageToClient(message);
}
protected void sendMessageToClient(Message<?> message) {
outboundChannel.send(message);
}
private void sendError(String sessionId, String errorText) {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.ERROR);
headers.setSessionId(sessionId);
headers.setMessage(errorText);
Message<?> errorMessage = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers.toMap()).build();
sendMessageToClient(errorMessage);
}
public void forward(Message<?> message, StompHeaderAccessor headers) {
if (!this.isConnected) {
synchronized(this.monitor) {
if (!this.isConnected) {
this.messageQueue.add(message);
if (logger.isTraceEnabled()) {
logger.trace("Queued message " + message + ", queue size=" + this.messageQueue.size());
}
return;
}
}
}
TcpConnection<String, String> connection = this.promise.get();
if (this.messageQueue.isEmpty()) {
forwardInternal(message, headers, connection);
}
else {
this.messageQueue.add(message);
flushMessages(connection);
}
}
private void flushMessages(TcpConnection<String, String> connection) {
List<Message<?>> messages = new ArrayList<Message<?>>();
this.messageQueue.drainTo(messages);
for (Message<?> message : messages) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
if (!forwardInternal(message, headers, connection)) {
return;
}
}
}
private boolean forwardInternal(Message<?> message, StompHeaderAccessor headers, TcpConnection<String, String> connection) {
try {
headers.setStompCommandIfNotSet(StompCommand.SEND);
if (logger.isTraceEnabled()) {
logger.trace("Forwarding message " + message);
}
byte[] bytes = stompMessageConverter.fromMessage(message);
connection.send(new String(bytes, Charset.forName("UTF-8")));
}
catch (Throwable ex) {
logger.error("Failed to forward message " + message, ex);
connection.close();
sendError(this.sessionId, "Failed to forward message " + message + ": " + ex.getMessage());
return false;
}
return true;
}
}
}

View File

@@ -1,255 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp.support;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.stomp.StompCommand;
import org.springframework.web.messaging.stomp.StompConversionException;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.TextWebSocketHandlerAdapter;
import reactor.util.Assert;
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class StompWebSocketHandler extends TextWebSocketHandlerAdapter implements MessageHandler {
private static final byte[] EMPTY_PAYLOAD = new byte[0];
private static Log logger = LogFactory.getLog(StompWebSocketHandler.class);
private MessageChannel outputChannel;
private final StompMessageConverter stompMessageConverter = new StompMessageConverter();
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<String, WebSocketSession>();
/**
* @param outputChannel the channel to which incoming STOMP/WebSocket messages should
* be sent to
*/
public StompWebSocketHandler(MessageChannel outputChannel) {
Assert.notNull(outputChannel, "clientInputChannel is required");
this.outputChannel = outputChannel;
}
public StompMessageConverter getStompMessageConverter() {
return this.stompMessageConverter;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Assert.notNull(this.outputChannel, "No output channel for STOMP messages.");
this.sessions.put(session.getId(), session);
}
/**
* Handle incoming WebSocket messages from clients.
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) {
try {
String payload = textMessage.getPayload();
Message<?> message = this.stompMessageConverter.toMessage(payload, session.getId());
// TODO: validate size limits
// http://stomp.github.io/stomp-specification-1.2.html#Size_Limits
if (logger.isTraceEnabled()) {
logger.trace("Processing STOMP message: " + message);
}
try {
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
MessageType messageType = stompHeaders.getMessageType();
if (MessageType.CONNECT.equals(messageType)) {
handleConnect(session, message);
}
else if (MessageType.MESSAGE.equals(messageType)) {
handlePublish(message);
}
else if (MessageType.SUBSCRIBE.equals(messageType)) {
handleSubscribe(message);
}
else if (MessageType.UNSUBSCRIBE.equals(messageType)) {
handleUnsubscribe(message);
}
else if (MessageType.DISCONNECT.equals(messageType)) {
handleDisconnect(message);
}
this.outputChannel.send(message);
}
catch (Throwable t) {
logger.error("Terminating STOMP session due to failure to send message: ", t);
sendErrorMessage(session, t);
}
// TODO: send RECEIPT message if incoming message has "receipt" header
// http://stomp.github.io/stomp-specification-1.2.html#Header_receipt
}
catch (Throwable error) {
sendErrorMessage(session, error);
}
}
protected void handleConnect(final WebSocketSession session, Message<?> message) throws IOException {
StompHeaderAccessor connectHeaders = StompHeaderAccessor.wrap(message);
StompHeaderAccessor connectedHeaders = StompHeaderAccessor.create(StompCommand.CONNECTED);
Set<String> acceptVersions = connectHeaders.getAcceptVersion();
if (acceptVersions.contains("1.2")) {
connectedHeaders.setAcceptVersion("1.2");
}
else if (acceptVersions.contains("1.1")) {
connectedHeaders.setAcceptVersion("1.1");
}
else if (acceptVersions.isEmpty()) {
// 1.0
}
else {
throw new StompConversionException("Unsupported version '" + acceptVersions + "'");
}
connectedHeaders.setHeartbeat(0,0); // TODO
// TODO: security
Message<?> connectedMessage = MessageBuilder.withPayload(EMPTY_PAYLOAD).copyHeaders(
connectedHeaders.toMap()).build();
byte[] bytes = this.stompMessageConverter.fromMessage(connectedMessage);
session.sendMessage(new TextMessage(new String(bytes, Charset.forName("UTF-8"))));
}
protected void handlePublish(Message<?> stompMessage) {
}
protected void handleSubscribe(Message<?> message) {
}
protected void handleUnsubscribe(Message<?> message) {
}
protected void handleDisconnect(Message<?> message) {
}
protected void sendErrorMessage(WebSocketSession session, Throwable error) {
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.ERROR);
headers.setMessage(error.getMessage());
Message<?> message = MessageBuilder.withPayload(EMPTY_PAYLOAD).copyHeaders(headers.toMap()).build();
byte[] bytes = this.stompMessageConverter.fromMessage(message);
try {
session.sendMessage(new TextMessage(new String(bytes, Charset.forName("UTF-8"))));
}
catch (Throwable t) {
// ignore
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
this.sessions.remove(session.getId());
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.DISCONNECT);
headers.setSessionId(session.getId());
Message<?> message = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers.toMap()).build();
this.outputChannel.send(message);
}
/**
* Handle STOMP messages going back out to WebSocket clients.
*/
@Override
public void handleMessage(Message<?> message) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
headers.setStompCommandIfNotSet(StompCommand.MESSAGE);
if (StompCommand.CONNECTED.equals(headers.getStompCommand())) {
// Ignore for now since we already sent it
return;
}
String sessionId = headers.getSessionId();
if (sessionId == null) {
// TODO: failed message delivery mechanism
logger.error("Ignoring message, no sessionId header: " + message);
return;
}
WebSocketSession session = this.sessions.get(sessionId);
if (session == null) {
// TODO: failed message delivery mechanism
logger.error("Ignoring message, session not found: " + sessionId);
return;
}
if (headers.getSubscriptionId() == null) {
// TODO: failed message delivery mechanism
logger.error("Ignoring message, no subscriptionId header: " + message);
return;
}
if (!(message.getPayload() instanceof byte[])) {
// TODO: failed message delivery mechanism
logger.error("Ignoring message, expected byte[] content: " + message);
return;
}
try {
message = MessageBuilder.fromMessage(message).copyHeaders(headers.toMap()).build();
byte[] bytes = this.stompMessageConverter.fromMessage(message);
session.sendMessage(new TextMessage(new String(bytes, Charset.forName("UTF-8"))));
}
catch (Throwable t) {
sendErrorMessage(session, t);
}
finally {
if (StompCommand.ERROR.equals(headers.getStompCommand())) {
try {
session.close(CloseStatus.PROTOCOL_ERROR);
}
catch (IOException e) {
}
}
}
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.support;
import org.springframework.core.NamedThreadLocal;
import org.springframework.messaging.Message;
// TODO: remove?
/**
* @author Rossen Stoyanchev
* @since 4.0
*/
public class MessageHolder {
private static final NamedThreadLocal<Message<?>> messageHolder =
new NamedThreadLocal<Message<?>>("Current message");
public static void setMessage(Message<?> message) {
messageHolder.set(message);
}
public static Message<?> getMessage() {
return messageHolder.get();
}
public static void reset() {
messageHolder.remove();
}
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.support;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.messaging.MessageType;
/**
* A base class for working with message headers in Web, messaging protocols that support
* the publish-subscribe message pattern. Provides uniform access to specific values
* common across protocols such as a destination, message type (publish,
* subscribe/unsubscribe), session id, and others.
* <p>
* Use one of the static factory method in this class, then call getters and setters, and
* at the end if necessary call {@link #toMap()} to obtain the updated headers.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class WebMessageHeaderAccesssor extends NativeMessageHeaderAccessor {
public static final String DESTINATIONS = "destinations";
public static final String CONTENT_TYPE = "contentType";
public static final String MESSAGE_TYPE = "messageType";
public static final String PROTOCOL_MESSAGE_TYPE = "protocolMessageType";
public static final String SESSION_ID = "sessionId";
public static final String SUBSCRIPTION_ID = "subscriptionId";
/**
* A constructor for creating new message headers.
* This constructor is protected. See factory methods in this and sub-classes.
*/
protected WebMessageHeaderAccesssor(MessageType messageType, Object protocolMessageType,
Map<String, List<String>> externalSourceHeaders) {
super(externalSourceHeaders);
Assert.notNull(messageType, "messageType is required");
setHeader(MESSAGE_TYPE, messageType);
if (protocolMessageType != null) {
setHeader(PROTOCOL_MESSAGE_TYPE, protocolMessageType);
}
}
/**
* A constructor for accessing and modifying existing message headers. This
* constructor is protected. See factory methods in this and sub-classes.
*/
protected WebMessageHeaderAccesssor(Message<?> message) {
super(message);
Assert.notNull(message, "message is required");
}
/**
* Create {@link WebMessageHeaderAccesssor} for a new {@link Message} with
* {@link MessageType#MESSAGE}.
*/
public static WebMessageHeaderAccesssor create() {
return new WebMessageHeaderAccesssor(MessageType.MESSAGE, null, null);
}
/**
* Create {@link WebMessageHeaderAccesssor} for a new {@link Message} of a specific type.
*/
public static WebMessageHeaderAccesssor create(MessageType messageType) {
return new WebMessageHeaderAccesssor(messageType, null, null);
}
/**
* Create {@link WebMessageHeaderAccesssor} from the headers of an existing message.
*/
public static WebMessageHeaderAccesssor wrap(Message<?> message) {
return new WebMessageHeaderAccesssor(message);
}
public MessageType getMessageType() {
return (MessageType) getHeader(MESSAGE_TYPE);
}
protected void setProtocolMessageType(Object protocolMessageType) {
setHeader(PROTOCOL_MESSAGE_TYPE, protocolMessageType);
}
protected Object getProtocolMessageType() {
return getHeader(PROTOCOL_MESSAGE_TYPE);
}
public void setDestination(String destination) {
Assert.notNull(destination, "destination is required");
setHeader(DESTINATIONS, Arrays.asList(destination));
}
@SuppressWarnings("unchecked")
public String getDestination() {
List<String> destinations = (List<String>) getHeader(DESTINATIONS);
return CollectionUtils.isEmpty(destinations) ? null : destinations.get(0);
}
@SuppressWarnings("unchecked")
public List<String> getDestinations() {
List<String> destinations = (List<String>) getHeader(DESTINATIONS);
return CollectionUtils.isEmpty(destinations) ? null : destinations;
}
public void setDestinations(List<String> destinations) {
Assert.notNull(destinations, "destinations are required");
setHeader(DESTINATIONS, destinations);
}
public MediaType getContentType() {
return (MediaType) getHeader(CONTENT_TYPE);
}
public void setContentType(MediaType contentType) {
Assert.notNull(contentType, "contentType is required");
setHeader(CONTENT_TYPE, contentType);
}
public String getSubscriptionId() {
return (String) getHeader(SUBSCRIPTION_ID);
}
public void setSubscriptionId(String subscriptionId) {
setHeader(SUBSCRIPTION_ID, subscriptionId);
}
public String getSessionId() {
return (String) getHeader(SESSION_ID);
}
public void setSessionId(String sessionId) {
setHeader(SESSION_ID, sessionId);
}
}

View File

@@ -1,64 +0,0 @@
package org.springframework.web.messaging.support;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.core.AbstractMessageSendingTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.web.messaging.MessageType;
public class WebMessagingTemplate extends AbstractMessageSendingTemplate<String> {
private final MessageChannel outputChannel;
private volatile long sendTimeout = -1;
public WebMessagingTemplate(MessageChannel outputChannel) {
Assert.notNull(outputChannel, "outputChannel is required");
this.outputChannel = outputChannel;
}
/**
* Specify the timeout value to use for send operations.
*
* @param sendTimeout the send timeout in milliseconds
*/
public void setSendTimeout(long sendTimeout) {
this.sendTimeout = sendTimeout;
}
@Override
public <P> void send(Message<P> message) {
// TODO: maybe look up destination of current message (via ThreadLocal)
this.send(getRequiredDefaultDestination(), message);
}
@Override
protected void doSend(String destination, Message<?> message) {
Assert.notNull(destination, "destination is required");
message = addDestinationToMessage(message, destination);
long timeout = this.sendTimeout;
boolean sent = (timeout >= 0)
? this.outputChannel.send(message, timeout)
: this.outputChannel.send(message);
if (!sent) {
throw new MessageDeliveryException(message,
"failed to send message to destination '" + destination + "' within timeout: " + timeout);
}
}
protected <P> Message<P> addDestinationToMessage(Message<P> message, String destination) {
Assert.notNull(destination, "destination is required");
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.MESSAGE);
headers.copyHeaders(message.getHeaders());
headers.setDestination(destination);
message = MessageBuilder.withPayload(message.getPayload()).copyHeaders(headers.toMap()).build();
return message;
}
}

View File

@@ -1,151 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.service.broker.SimpleBrokerWebMessageHandler;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class SimpleBrokerWebMessageHandlerTests {
private AbstractWebMessageHandler messageHandler;
@Mock
private MessageChannel clientChannel;
@Captor
ArgumentCaptor<Message<?>> messageCaptor;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.messageHandler = new SimpleBrokerWebMessageHandler(this.clientChannel);
}
@Test
public void getSupportedMessageTypes() {
assertEquals(Arrays.asList(MessageType.MESSAGE, MessageType.SUBSCRIBE, MessageType.UNSUBSCRIBE),
this.messageHandler.getSupportedMessageTypes());
}
@Test
public void subcribePublish() {
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess1", "sub1", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess1", "sub2", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess1", "sub3", "/bar"));
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess2", "sub1", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess2", "sub2", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage("sess2", "sub3", "/bar"));
this.messageHandler.handlePublish(createMessage("/foo", "message1"));
this.messageHandler.handlePublish(createMessage("/bar", "message2"));
verify(this.clientChannel, times(6)).send(this.messageCaptor.capture());
assertCapturedMessage("sess1", "sub1", "/foo");
assertCapturedMessage("sess1", "sub2", "/foo");
assertCapturedMessage("sess2", "sub1", "/foo");
assertCapturedMessage("sess2", "sub2", "/foo");
assertCapturedMessage("sess1", "sub3", "/bar");
assertCapturedMessage("sess2", "sub3", "/bar");
}
@Test
public void subcribeDisconnectPublish() {
String sess1 = "sess1";
String sess2 = "sess2";
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess1, "sub1", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess1, "sub2", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess1, "sub3", "/bar"));
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess2, "sub1", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess2, "sub2", "/foo"));
this.messageHandler.handleSubscribe(createSubscriptionMessage(sess2, "sub3", "/bar"));
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.DISCONNECT);
headers.setSessionId(sess1);
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers.toMap()).build();
this.messageHandler.handleDisconnect(message);
this.messageHandler.handlePublish(createMessage("/foo", "message1"));
this.messageHandler.handlePublish(createMessage("/bar", "message2"));
verify(this.clientChannel, times(3)).send(this.messageCaptor.capture());
assertCapturedMessage(sess2, "sub1", "/foo");
assertCapturedMessage(sess2, "sub2", "/foo");
assertCapturedMessage(sess2, "sub3", "/bar");
}
protected Message<String> createSubscriptionMessage(String sessionId, String subcriptionId, String destination) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.SUBSCRIBE);
headers.setSubscriptionId(subcriptionId);
headers.setDestination(destination);
headers.setSessionId(sessionId);
return MessageBuilder.withPayload("").copyHeaders(headers.toMap()).build();
}
protected Message<String> createMessage(String destination, String payload) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.MESSAGE);
headers.setDestination(destination);
return MessageBuilder.withPayload(payload).copyHeaders(headers.toMap()).build();
}
protected boolean assertCapturedMessage(String sessionId, String subcriptionId, String destination) {
for (Message<?> message : this.messageCaptor.getAllValues()) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.wrap(message);
if (sessionId.equals(headers.getSessionId())) {
if (subcriptionId.equals(headers.getSubscriptionId())) {
if (destination.equals(headers.getDestination())) {
return true;
}
}
}
}
return false;
}
}

View File

@@ -1,242 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.service.broker;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MultiValueMap;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import static org.junit.Assert.*;
/**
* Test fixture for {@link DefaultSubscriptionRegistry}.
*
* @author Rossen Stoyanchev
*/
public class DefaultSubscriptionRegistryTests {
private DefaultSubscriptionRegistry registry;
@Before
public void setup() {
this.registry = new DefaultSubscriptionRegistry();
}
@Test
public void addSubscriptionInvalidInput() {
String sessId = "sess01";
String subsId = "subs01";
String dest = "/foo";
this.registry.addSubscription(subscribeMessage(null, subsId, dest));
assertEquals(0, this.registry.findSubscriptions(message(dest)).size());
this.registry.addSubscription(subscribeMessage(sessId, null, dest));
assertEquals(0, this.registry.findSubscriptions(message(dest)).size());
this.registry.addSubscription(subscribeMessage(sessId, subsId, null));
assertEquals(0, this.registry.findSubscriptions(message(dest)).size());
}
@Test
public void addSubscription() {
String sessId = "sess01";
String subsId = "subs01";
String dest = "/foo";
this.registry.addSubscription(subscribeMessage(sessId, subsId, dest));
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected one element " + actual, 1, actual.size());
assertEquals(Arrays.asList(subsId), actual.get(sessId));
}
@Test
public void addSubscriptionOneSession() {
String sessId = "sess01";
List<String> subscriptionIds = Arrays.asList("subs01", "subs02", "subs03");
String dest = "/foo";
for (String subId : subscriptionIds) {
this.registry.addSubscription(subscribeMessage(sessId, subId, dest));
}
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected one element " + actual, 1, actual.size());
assertEquals(subscriptionIds, sort(actual.get(sessId)));
}
@Test
public void addSubscriptionMultipleSessions() {
List<String> sessIds = Arrays.asList("sess01", "sess02", "sess03");
List<String> subscriptionIds = Arrays.asList("subs01", "subs02", "subs03");
String dest = "/foo";
for (String sessId : sessIds) {
for (String subsId : subscriptionIds) {
this.registry.addSubscription(subscribeMessage(sessId, subsId, dest));
}
}
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected three elements " + actual, 3, actual.size());
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(0))));
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(1))));
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(2))));
}
@Test
public void addSubscriptionWithDestinationPattern() {
String sessId = "sess01";
String subsId = "subs01";
String destPattern = "/topic/PRICE.STOCK.*.IBM";
String dest = "/topic/PRICE.STOCK.NASDAQ.IBM";
this.registry.addSubscription(subscribeMessage(sessId, subsId, destPattern));
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected one element " + actual, 1, actual.size());
assertEquals(Arrays.asList(subsId), actual.get(sessId));
}
@Test
public void addSubscriptionWithDestinationPatternRegex() {
String sessId = "sess01";
String subsId = "subs01";
String destPattern = "/topic/PRICE.STOCK.*.{ticker:(IBM|MSFT)}";
this.registry.addSubscription(subscribeMessage(sessId, subsId, destPattern));
Message<?> message = message("/topic/PRICE.STOCK.NASDAQ.IBM");
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
assertEquals("Expected one element " + actual, 1, actual.size());
assertEquals(Arrays.asList(subsId), actual.get(sessId));
message = message("/topic/PRICE.STOCK.NASDAQ.MSFT");
actual = this.registry.findSubscriptions(message);
assertEquals("Expected one element " + actual, 1, actual.size());
assertEquals(Arrays.asList(subsId), actual.get(sessId));
message = message("/topic/PRICE.STOCK.NASDAQ.VMW");
actual = this.registry.findSubscriptions(message);
assertEquals("Expected no elements " + actual, 0, actual.size());
}
@Test
public void removeSubscription() {
List<String> sessIds = Arrays.asList("sess01", "sess02", "sess03");
List<String> subscriptionIds = Arrays.asList("subs01", "subs02", "subs03");
String dest = "/foo";
for (String sessId : sessIds) {
for (String subsId : subscriptionIds) {
this.registry.addSubscription(subscribeMessage(sessId, subsId, dest));
}
}
this.registry.removeSubscription(unsubscribeMessage(sessIds.get(0), subscriptionIds.get(0)));
this.registry.removeSubscription(unsubscribeMessage(sessIds.get(0), subscriptionIds.get(1)));
this.registry.removeSubscription(unsubscribeMessage(sessIds.get(0), subscriptionIds.get(2)));
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected three elements " + actual, 2, actual.size());
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(1))));
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(2))));
}
@Test
public void removeSessionSubscriptions() {
List<String> sessIds = Arrays.asList("sess01", "sess02", "sess03");
List<String> subscriptionIds = Arrays.asList("subs01", "subs02", "subs03");
String dest = "/foo";
for (String sessId : sessIds) {
for (String subsId : subscriptionIds) {
this.registry.addSubscription(subscribeMessage(sessId, subsId, dest));
}
}
this.registry.removeSessionSubscriptions(sessIds.get(0));
this.registry.removeSessionSubscriptions(sessIds.get(1));
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message(dest));
assertEquals("Expected three elements " + actual, 1, actual.size());
assertEquals(subscriptionIds, sort(actual.get(sessIds.get(2))));
}
@Test
public void findSubscriptionsNoMatches() {
MultiValueMap<String, String> actual = this.registry.findSubscriptions(message("/foo"));
assertEquals("Expected no elements " + actual, 0, actual.size());
}
private Message<?> subscribeMessage(String sessionId, String subscriptionId, String destination) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.SUBSCRIBE);
headers.setSessionId(sessionId);
headers.setSubscriptionId(subscriptionId);
if (destination != null) {
headers.setDestination(destination);
}
return MessageBuilder.withPayload("").copyHeaders(headers.toMap()).build();
}
private Message<?> unsubscribeMessage(String sessionId, String subscriptionId) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create(MessageType.UNSUBSCRIBE);
headers.setSessionId(sessionId);
headers.setSubscriptionId(subscriptionId);
return MessageBuilder.withPayload("").copyHeaders(headers.toMap()).build();
}
private Message<?> message(String destination) {
WebMessageHeaderAccesssor headers = WebMessageHeaderAccesssor.create();
headers.setDestination(destination);
return MessageBuilder.withPayload("").copyHeaders(headers.toMap()).build();
}
private List<String> sort(List<String> list) {
Collections.sort(list);
return list;
}
}

View File

@@ -1,149 +0,0 @@
/*
* Copyright 2002-2013 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.messaging.stomp.support;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.web.messaging.MessageType;
import org.springframework.web.messaging.stomp.StompCommand;
import org.springframework.web.messaging.support.WebMessageHeaderAccesssor;
import static org.junit.Assert.*;
/**
* @author Gary Russell
* @author Rossen Stoyanchev
*/
public class StompMessageConverterTests {
private StompMessageConverter converter;
@Before
public void setup() {
this.converter = new StompMessageConverter();
}
@SuppressWarnings("unchecked")
@Test
public void connectFrame() throws Exception {
String accept = "accept-version:1.1\n";
String host = "host:github.org\n";
String frame = "\n\n\nCONNECT\n" + accept + host + "\n";
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(frame.getBytes("UTF-8"), "session-123");
assertEquals(0, message.getPayload().length);
MessageHeaders headers = message.getHeaders();
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
Map<String, Object> map = stompHeaders.toMap();
assertEquals(6, map.size());
assertNotNull(map.get(MessageHeaders.ID));
assertNotNull(map.get(MessageHeaders.TIMESTAMP));
assertNotNull(map.get(WebMessageHeaderAccesssor.SESSION_ID));
assertNotNull(map.get(WebMessageHeaderAccesssor.NATIVE_HEADERS));
assertNotNull(map.get(WebMessageHeaderAccesssor.MESSAGE_TYPE));
assertNotNull(map.get(WebMessageHeaderAccesssor.PROTOCOL_MESSAGE_TYPE));
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion());
assertEquals("github.org", stompHeaders.getHost());
assertEquals(MessageType.CONNECT, stompHeaders.getMessageType());
assertEquals(StompCommand.CONNECT, stompHeaders.getStompCommand());
assertEquals("session-123", stompHeaders.getSessionId());
assertNotNull(headers.get(MessageHeaders.ID));
assertNotNull(headers.get(MessageHeaders.TIMESTAMP));
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8");
assertEquals("CONNECT\n", convertedBack.substring(0,8));
assertTrue(convertedBack.contains(accept));
assertTrue(convertedBack.contains(host));
}
@Test
public void connectWithEscapes() throws Exception {
String accept = "accept-version:1.1\n";
String host = "ho\\c\\ns\\rt:st\\nomp.gi\\cthu\\b.org\n";
String frame = "CONNECT\n" + accept + host + "\n";
@SuppressWarnings("unchecked")
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(frame.getBytes("UTF-8"), "session-123");
assertEquals(0, message.getPayload().length);
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion());
assertEquals("st\nomp.gi:thu\\b.org", stompHeaders.toNativeHeaderMap().get("ho:\ns\rt").get(0));
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8");
assertEquals("CONNECT\n", convertedBack.substring(0,8));
assertTrue(convertedBack.contains(accept));
assertTrue(convertedBack.contains(host));
}
@Test
public void connectCR12() throws Exception {
String accept = "accept-version:1.2\n";
String host = "host:github.org\n";
String test = "CONNECT\r\n" + accept.replaceAll("\n", "\r\n") + host.replaceAll("\n", "\r\n") + "\r\n";
@SuppressWarnings("unchecked")
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(test.getBytes("UTF-8"), "session-123");
assertEquals(0, message.getPayload().length);
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
assertEquals(Collections.singleton("1.2"), stompHeaders.getAcceptVersion());
assertEquals("github.org", stompHeaders.getHost());
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8");
assertEquals("CONNECT\n", convertedBack.substring(0,8));
assertTrue(convertedBack.contains(accept));
assertTrue(convertedBack.contains(host));
}
@Test
public void connectWithEscapesAndCR12() throws Exception {
String accept = "accept-version:1.1\n";
String host = "ho\\c\\ns\\rt:st\\nomp.gi\\cthu\\b.org\n";
String test = "\n\n\nCONNECT\r\n" + accept.replaceAll("\n", "\r\n") + host.replaceAll("\n", "\r\n") + "\r\n";
@SuppressWarnings("unchecked")
Message<byte[]> message = (Message<byte[]>) this.converter.toMessage(test.getBytes("UTF-8"), "session-123");
assertEquals(0, message.getPayload().length);
StompHeaderAccessor stompHeaders = StompHeaderAccessor.wrap(message);
assertEquals(Collections.singleton("1.1"), stompHeaders.getAcceptVersion());
assertEquals("st\nomp.gi:thu\\b.org", stompHeaders.toNativeHeaderMap().get("ho:\ns\rt").get(0));
String convertedBack = new String(this.converter.fromMessage(message), "UTF-8");
assertEquals("CONNECT\n", convertedBack.substring(0,8));
assertTrue(convertedBack.contains(accept));
assertTrue(convertedBack.contains(host));
}
}