Create spring-messaging module
Consolidates new, messaging-related classes from spring-context and spring-websocket into one module.
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user