Create spring-messaging module
Consolidates new, messaging-related classes from spring-context and spring-websocket into one module.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* A generic message representation with headers and body.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Arjen Poutsma
|
||||
* @since 4.0
|
||||
* @see org.springframework.messaging.support.MessageBuilder
|
||||
*/
|
||||
public interface Message<T> {
|
||||
|
||||
/**
|
||||
* Returns message headers for the message (never {@code null}).
|
||||
*/
|
||||
MessageHeaders getHeaders();
|
||||
|
||||
/**
|
||||
* Returns the message payload.
|
||||
*/
|
||||
T getPayload();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* Base channel interface defining common behavior for sending messages.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageChannel {
|
||||
|
||||
/**
|
||||
* Constant for sending a message without a prescribed timeout.
|
||||
*/
|
||||
public static final long INDEFINITE_TIMEOUT = -1;
|
||||
|
||||
|
||||
/**
|
||||
* Send a {@link Message} to this channel. May throw a RuntimeException for
|
||||
* non-recoverable errors. Otherwise, if the Message cannot be sent for a non-fatal
|
||||
* reason this method will return 'false', and if the Message is sent successfully, it
|
||||
* will return 'true'.
|
||||
*
|
||||
* <p>Depending on the implementation, this method may block indefinitely. To provide a
|
||||
* maximum wait time, use {@link #send(Message, long)}.
|
||||
* @param message the {@link Message} to send
|
||||
* @return whether or not the Message has been sent successfully
|
||||
*/
|
||||
boolean send(Message<?> message);
|
||||
|
||||
/**
|
||||
* Send a message, blocking until either the message is accepted or the specified
|
||||
* timeout period elapses.
|
||||
* @param message the {@link Message} to send
|
||||
* @param timeout the timeout in milliseconds or #INDEFINITE_TIMEOUT
|
||||
* @return {@code true} if the message is sent successfully, {@code false} if the
|
||||
* specified timeout period elapses or the send is interrupted
|
||||
*/
|
||||
boolean send(Message<?> message, long timeout);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* Exception that indicates an error occurred during message delivery.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MessageDeliveryException extends MessagingException {
|
||||
|
||||
public MessageDeliveryException(String description) {
|
||||
super(description);
|
||||
}
|
||||
|
||||
public MessageDeliveryException(Message<?> undeliveredMessage) {
|
||||
super(undeliveredMessage);
|
||||
}
|
||||
|
||||
public MessageDeliveryException(Message<?> undeliveredMessage, String description) {
|
||||
super(undeliveredMessage, description);
|
||||
}
|
||||
|
||||
public MessageDeliveryException(Message<?> undeliveredMessage, String description, Throwable cause) {
|
||||
super(undeliveredMessage, description, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* Base interface for any component that handles Messages.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Iwein Fuld
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageHandler {
|
||||
|
||||
/**
|
||||
* Handles the message if possible. If the handler cannot deal with the
|
||||
* message this will result in a {@code MessageRejectedException} e.g.
|
||||
* in case of a Selective Consumer. When a consumer tries to handle a
|
||||
* message, but fails to do so, a {@code MessageHandlingException} is
|
||||
* thrown. In the last case it is recommended to treat the message as tainted
|
||||
* and go into an error scenario.
|
||||
* <p>
|
||||
* When the handling results in a failure of another message being sent
|
||||
* (e.g. a "reply" message), that failure will trigger a
|
||||
* {@code MessageDeliveryException}.
|
||||
*
|
||||
* @param message the message to be handled
|
||||
* reply related to the handling of the message
|
||||
*/
|
||||
void handleMessage(Message<?> message) throws MessagingException;
|
||||
|
||||
/*
|
||||
* TODO: exceptions
|
||||
* @throws org.springframework.integration.MessageRejectedException if the handler doesn't accept the message
|
||||
* @throws org.springframework.integration.MessageHandlingException when something fails during the handling
|
||||
* @throws org.springframework.integration.MessageDeliveryException when this handler failed to deliver the
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* The headers for a {@link Message}
|
||||
*
|
||||
* <p><b>IMPORTANT</b>: This class is immutable. Any mutating operation
|
||||
* (e.g., put(..), putAll(..) etc.) will throw {@link UnsupportedOperationException}.
|
||||
*
|
||||
* <p>To create MessageHeaders instance use fluent
|
||||
* {@link org.springframework.messaging.support.MessageBuilder MessageBuilder} API
|
||||
* <pre class="code">
|
||||
* MessageBuilder.withPayload("foo").setHeader("key1", "value1").setHeader("key2", "value2");
|
||||
* </pre>
|
||||
* or create an instance of GenericMessage passing payload as {@link Object} and headers as a regular {@link Map}
|
||||
* <pre class="code">
|
||||
* Map headers = new HashMap();
|
||||
* headers.put("key1", "value1");
|
||||
* headers.put("key2", "value2");
|
||||
* new GenericMessage("foo", headers);
|
||||
* </pre>
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 4.0
|
||||
* @see org.springframework.messaging.support.MessageBuilder
|
||||
*/
|
||||
public final class MessageHeaders implements Map<String, Object>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = -4615750558355702881L;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(MessageHeaders.class);
|
||||
|
||||
|
||||
private static volatile IdGenerator idGenerator = null;
|
||||
|
||||
/**
|
||||
* The key for the Message ID. This is an automatically generated UUID and
|
||||
* should never be explicitly set in the header map <b>except</b> in the
|
||||
* case of Message deserialization where the serialized Message's generated
|
||||
* UUID is being restored.
|
||||
*/
|
||||
public static final String ID = "id";
|
||||
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
|
||||
public static final String REPLY_CHANNEL = "replyChannel";
|
||||
|
||||
public static final String ERROR_CHANNEL = "errorChannel";
|
||||
|
||||
public static final String CONTENT_TYPE = "contentType";
|
||||
|
||||
public static final List<String> HEADER_NAMES = Arrays.asList(ID, TIMESTAMP);
|
||||
|
||||
|
||||
private final Map<String, Object> headers;
|
||||
|
||||
|
||||
public MessageHeaders(Map<String, Object> headers) {
|
||||
this.headers = (headers != null) ? new HashMap<String, Object>(headers) : new HashMap<String, Object>();
|
||||
if (MessageHeaders.idGenerator == null){
|
||||
this.headers.put(ID, UUID.randomUUID());
|
||||
}
|
||||
else {
|
||||
this.headers.put(ID, MessageHeaders.idGenerator.generateId());
|
||||
}
|
||||
|
||||
this.headers.put(TIMESTAMP, new Long(System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return this.get(ID, UUID.class);
|
||||
}
|
||||
|
||||
public Long getTimestamp() {
|
||||
return this.get(TIMESTAMP, Long.class);
|
||||
}
|
||||
|
||||
public Object getReplyChannel() {
|
||||
return this.get(REPLY_CHANNEL);
|
||||
}
|
||||
|
||||
public Object getErrorChannel() {
|
||||
return this.get(ERROR_CHANNEL);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(Object key, Class<T> type) {
|
||||
Object value = this.headers.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (!type.isAssignableFrom(value.getClass())) {
|
||||
throw new IllegalArgumentException("Incorrect type specified for header '" + key + "'. Expected [" + type
|
||||
+ "] but actual type is [" + value.getClass() + "]");
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.headers.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (object != null && object instanceof MessageHeaders) {
|
||||
MessageHeaders other = (MessageHeaders) object;
|
||||
return this.headers.equals(other.headers);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.headers.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
* Map implementation
|
||||
*/
|
||||
|
||||
public boolean containsKey(Object key) {
|
||||
return this.headers.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean containsValue(Object value) {
|
||||
return this.headers.containsValue(value);
|
||||
}
|
||||
|
||||
public Set<Map.Entry<String, Object>> entrySet() {
|
||||
return Collections.unmodifiableSet(this.headers.entrySet());
|
||||
}
|
||||
|
||||
public Object get(Object key) {
|
||||
return this.headers.get(key);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return this.headers.isEmpty();
|
||||
}
|
||||
|
||||
public Set<String> keySet() {
|
||||
return Collections.unmodifiableSet(this.headers.keySet());
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return this.headers.size();
|
||||
}
|
||||
|
||||
public Collection<Object> values() {
|
||||
return Collections.unmodifiableCollection(this.headers.values());
|
||||
}
|
||||
|
||||
// Unsupported operations
|
||||
|
||||
/**
|
||||
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
|
||||
*/
|
||||
public Object put(String key, Object value) {
|
||||
throw new UnsupportedOperationException("MessageHeaders is immutable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
|
||||
*/
|
||||
public void putAll(Map<? extends String, ? extends Object> t) {
|
||||
throw new UnsupportedOperationException("MessageHeaders is immutable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
|
||||
*/
|
||||
public Object remove(Object key) {
|
||||
throw new UnsupportedOperationException("MessageHeaders is immutable.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Since MessageHeaders are immutable the call to this method will result in {@link UnsupportedOperationException}
|
||||
*/
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException("MessageHeaders is immutable.");
|
||||
}
|
||||
|
||||
// Serialization methods
|
||||
|
||||
private void writeObject(ObjectOutputStream out) throws IOException {
|
||||
List<String> keysToRemove = new ArrayList<String>();
|
||||
for (Map.Entry<String, Object> entry : this.headers.entrySet()) {
|
||||
if (!(entry.getValue() instanceof Serializable)) {
|
||||
keysToRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (String key : keysToRemove) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("removing non-serializable header: " + key);
|
||||
}
|
||||
this.headers.remove(key);
|
||||
}
|
||||
out.defaultWriteObject();
|
||||
}
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
}
|
||||
|
||||
public static interface IdGenerator {
|
||||
UUID generateId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* The base exception for any failures related to messaging.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MessagingException extends RuntimeException {
|
||||
|
||||
private volatile Message<?> failedMessage;
|
||||
|
||||
|
||||
public MessagingException(Message<?> message) {
|
||||
super();
|
||||
this.failedMessage = message;
|
||||
}
|
||||
|
||||
public MessagingException(String description) {
|
||||
super(description);
|
||||
this.failedMessage = null;
|
||||
}
|
||||
|
||||
public MessagingException(String description, Throwable cause) {
|
||||
super(description, cause);
|
||||
this.failedMessage = null;
|
||||
}
|
||||
|
||||
public MessagingException(Message<?> message, String description) {
|
||||
super(description);
|
||||
this.failedMessage = message;
|
||||
}
|
||||
|
||||
public MessagingException(Message<?> message, Throwable cause) {
|
||||
super(cause);
|
||||
this.failedMessage = message;
|
||||
}
|
||||
|
||||
public MessagingException(Message<?> message, String description, Throwable cause) {
|
||||
super(description, cause);
|
||||
this.failedMessage = message;
|
||||
}
|
||||
|
||||
|
||||
public Message<?> getFailedMessage() {
|
||||
return this.failedMessage;
|
||||
}
|
||||
|
||||
public void setFailedMessage(Message<?> message) {
|
||||
this.failedMessage = message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
/**
|
||||
* Interface for Message Channels from which Messages may be actively received through
|
||||
* polling.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface PollableChannel extends MessageChannel {
|
||||
|
||||
/**
|
||||
* Receive a message from this channel, blocking indefinitely if necessary.
|
||||
* @return the next available {@link Message} or {@code null} if interrupted
|
||||
*/
|
||||
Message<?> receive();
|
||||
|
||||
/**
|
||||
* Receive a message from this channel, blocking until either a message is available
|
||||
* or the specified timeout period elapses.
|
||||
* @param timeout the timeout in milliseconds or
|
||||
* {@link MessageChannel#INDEFINITE_TIMEOUT}.
|
||||
* @return the next available {@link Message} or {@code null} if the specified timeout
|
||||
* period elapses or the message reception is interrupted
|
||||
*/
|
||||
Message<?> receive(long timeout);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.messaging;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for any MessageChannel implementation that accepts subscribers.
|
||||
* The subscribers must implement the {@link MessageHandler} interface and
|
||||
* will be invoked when a Message is available.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface SubscribableChannel extends MessageChannel {
|
||||
|
||||
/**
|
||||
* Register a {@link MessageHandler} as a subscriber to this channel.
|
||||
* @return {@code true} if the channel was not already subscribed to the specified
|
||||
* handler
|
||||
*/
|
||||
boolean subscribe(MessageHandler handler);
|
||||
|
||||
/**
|
||||
* Remove a {@link MessageHandler} from the subscribers of this channel.
|
||||
* @return {@code true} if the channel was previously subscribed to the specified
|
||||
* handler
|
||||
*/
|
||||
boolean unsubscribe(MessageHandler handler);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractDestinationResolvingMessagingTemplate<D> extends AbstractMessagingTemplate<D>
|
||||
implements DestinationResolvingMessageSendingOperations<D>,
|
||||
DestinationResolvingMessageReceivingOperations<D>,
|
||||
DestinationResolvingMessageRequestReplyOperations<D> {
|
||||
|
||||
private volatile DestinationResolver<D> destinationResolver;
|
||||
|
||||
|
||||
public void setDestinationResolver(DestinationResolver<D> destinationResolver) {
|
||||
this.destinationResolver = destinationResolver;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <P> void send(String destinationName, Message<P> message) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
this.doSend(destination, message);
|
||||
}
|
||||
|
||||
protected final D resolveDestination(String destinationName) {
|
||||
Assert.notNull(destinationResolver, "destinationResolver is required when passing a name only");
|
||||
return this.destinationResolver.resolveDestination(destinationName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(String destinationName, T message) {
|
||||
this.convertAndSend(destinationName, message, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(String destinationName, T message, MessagePostProcessor postProcessor) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
super.convertAndSend(destination, message, postProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <P> Message<P> receive(String destinationName) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
return super.receive(destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receiveAndConvert(String destinationName) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
return super.receiveAndConvert(destination);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> sendAndReceive(String destinationName, Message<?> requestMessage) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
return super.sendAndReceive(destination, requestMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String destinationName, Object request) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
return super.convertSendAndReceive(destination, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String destinationName, Object request, MessagePostProcessor postProcessor) {
|
||||
D destination = resolveDestination(destinationName);
|
||||
return super.convertSendAndReceive(destination, request, postProcessor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.converter.SimplePayloadMessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractMessageSendingTemplate<D> implements MessageSendingOperations<D> {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private volatile D defaultDestination;
|
||||
|
||||
private volatile MessageConverter converter = new SimplePayloadMessageConverter();
|
||||
|
||||
|
||||
public void setDefaultDestination(D defaultDestination) {
|
||||
this.defaultDestination = defaultDestination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link MessageConverter} that is to be used to convert
|
||||
* between Messages and objects for this template.
|
||||
* <p>The default is {@link SimplePayloadMessageConverter}.
|
||||
*/
|
||||
public void setMessageConverter(MessageConverter messageConverter) {
|
||||
Assert.notNull(messageConverter, "'messageConverter' must not be null");
|
||||
this.converter = messageConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the configured {@link MessageConverter}
|
||||
*/
|
||||
public MessageConverter getConverter() {
|
||||
return this.converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param converter the converter to set
|
||||
*/
|
||||
public void setConverter(MessageConverter converter) {
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <P> void send(Message<P> message) {
|
||||
this.send(getRequiredDefaultDestination(), message);
|
||||
}
|
||||
|
||||
protected final D getRequiredDefaultDestination() {
|
||||
Assert.state(this.defaultDestination != null,
|
||||
"No 'defaultDestination' specified for MessagingTemplate. "
|
||||
+ "Unable to invoke method without an explicit destination argument.");
|
||||
return this.defaultDestination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <P> void send(D destination, Message<P> message) {
|
||||
this.doSend(destination, message);
|
||||
}
|
||||
|
||||
protected abstract void doSend(D destination, Message<?> message) ;
|
||||
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(T message) {
|
||||
this.convertAndSend(getRequiredDefaultDestination(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(D destination, T object) {
|
||||
this.convertAndSend(destination, object, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(T object, MessagePostProcessor postProcessor) {
|
||||
this.convertAndSend(getRequiredDefaultDestination(), object, postProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void convertAndSend(D destination, T object, MessagePostProcessor postProcessor)
|
||||
throws MessagingException {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<?> message = this.converter.toMessage(object);
|
||||
if (postProcessor != null) {
|
||||
message = postProcessor.postProcessMessage(message);
|
||||
}
|
||||
this.send(destination, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractMessagingTemplate<D> extends AbstractMessageSendingTemplate<D>
|
||||
implements MessageRequestReplyOperations<D>, MessageReceivingOperations<D> {
|
||||
|
||||
|
||||
@Override
|
||||
public <P> Message<P> receive() {
|
||||
return this.receive(getRequiredDefaultDestination());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <P> Message<P> receive(D destination) {
|
||||
return this.doReceive(destination);
|
||||
}
|
||||
|
||||
protected abstract <P> Message<P> doReceive(D destination);
|
||||
|
||||
|
||||
@Override
|
||||
public Object receiveAndConvert() {
|
||||
return this.receiveAndConvert(getRequiredDefaultDestination());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object receiveAndConvert(D destination) {
|
||||
Message<?> message = this.doReceive(destination);
|
||||
return (message != null) ? getConverter().fromMessage(message, null) : null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Message<?> sendAndReceive(Message<?> requestMessage) {
|
||||
return this.sendAndReceive(getRequiredDefaultDestination(), requestMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> sendAndReceive(D destination, Message<?> requestMessage) {
|
||||
return this.doSendAndReceive(destination, requestMessage);
|
||||
}
|
||||
|
||||
protected abstract <S, R> Message<R> doSendAndReceive(D destination, Message<S> requestMessage);
|
||||
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(Object request) {
|
||||
return this.convertSendAndReceive(getRequiredDefaultDestination(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(D destination, Object request) {
|
||||
return this.convertSendAndReceive(destination, request, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(Object request, MessagePostProcessor postProcessor) {
|
||||
return this.convertSendAndReceive(getRequiredDefaultDestination(), request, postProcessor);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object convertSendAndReceive(D destination, Object request, MessagePostProcessor postProcessor) {
|
||||
Message<?> requestMessage = getConverter().toMessage(request);
|
||||
if (postProcessor != null) {
|
||||
requestMessage = postProcessor.postProcessMessage(requestMessage);
|
||||
}
|
||||
Message<?> replyMessage = this.sendAndReceive(destination, requestMessage);
|
||||
return getConverter().fromMessage(replyMessage, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public class BeanFactoryMessageChannelDestinationResolver implements DestinationResolver<MessageChannel> {
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
|
||||
public BeanFactoryMessageChannelDestinationResolver(BeanFactory beanFactory) {
|
||||
Assert.notNull(beanFactory, "beanFactory must not be null");
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public MessageChannel resolveDestination(String name) {
|
||||
Assert.state(this.beanFactory != null, "BeanFactory is required");
|
||||
try {
|
||||
return this.beanFactory.getBean(name, MessageChannel.class);
|
||||
}
|
||||
catch (BeansException e) {
|
||||
throw new DestinationResolutionException(
|
||||
"failed to look up MessageChannel bean with name '" + name + "'", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
/**
|
||||
* Thrown by a ChannelResolver when it cannot resolve a channel name.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class DestinationResolutionException extends MessagingException {
|
||||
|
||||
/**
|
||||
* Create a new ChannelResolutionException.
|
||||
* @param description the description
|
||||
*/
|
||||
public DestinationResolutionException(String description) {
|
||||
super(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ChannelResolutionException.
|
||||
* @param description the description
|
||||
* @param cause the root cause (if any)
|
||||
*/
|
||||
public DestinationResolutionException(String description, Throwable cause) {
|
||||
super(description, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface DestinationResolver<D> {
|
||||
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @return
|
||||
* @throws DestinationResolutionException
|
||||
*/
|
||||
D resolveDestination(String name) throws DestinationResolutionException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface DestinationResolvingMessageReceivingOperations<D> extends MessageReceivingOperations<D> {
|
||||
|
||||
<P> Message<P> receive(String destinationName) throws MessagingException;
|
||||
|
||||
Object receiveAndConvert(String destinationName) throws MessagingException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface DestinationResolvingMessageRequestReplyOperations<D> extends MessageRequestReplyOperations<D> {
|
||||
|
||||
Message<?> sendAndReceive(String destinationName, Message<?> requestMessage);
|
||||
|
||||
Object convertSendAndReceive(String destinationName, Object request);
|
||||
|
||||
Object convertSendAndReceive(String destinationName, Object request, MessagePostProcessor requestPostProcessor);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface DestinationResolvingMessageSendingOperations<D> extends MessageSendingOperations<D> {
|
||||
|
||||
<P> void send(String destinationName, Message<P> message) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(String destinationName, T message) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(String destinationName, T message, MessagePostProcessor postProcessor)
|
||||
throws MessagingException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageDeliveryException;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public class GenericMessagingTemplate extends AbstractDestinationResolvingMessagingTemplate<MessageChannel>
|
||||
implements BeanFactoryAware {
|
||||
|
||||
private volatile long sendTimeout = -1;
|
||||
|
||||
private volatile long receiveTimeout = -1;
|
||||
|
||||
private volatile boolean throwExceptionOnLateReply = false;
|
||||
|
||||
|
||||
/**
|
||||
* Specify the timeout value to use for send operations.
|
||||
*
|
||||
* @param sendTimeout the send timeout in milliseconds
|
||||
*/
|
||||
public void setSendTimeout(long sendTimeout) {
|
||||
this.sendTimeout = sendTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the timeout value to use for receive operations.
|
||||
*
|
||||
* @param receiveTimeout the receive timeout in milliseconds
|
||||
*/
|
||||
public void setReceiveTimeout(long receiveTimeout) {
|
||||
this.receiveTimeout = receiveTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify whether or not an attempt to send on the reply channel throws an exception
|
||||
* if no receiving thread will actually receive the reply. This can occur
|
||||
* if the receiving thread has already timed out, or will never call receive()
|
||||
* because it caught an exception, or has already received a reply.
|
||||
* (default false - just a WARN log is emitted in these cases).
|
||||
* @param throwExceptionOnLateReply TRUE or FALSE.
|
||||
*/
|
||||
public void setThrowExceptionOnLateReply(boolean throwExceptionOnLateReply) {
|
||||
this.throwExceptionOnLateReply = throwExceptionOnLateReply;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
super.setDestinationResolver(new BeanFactoryMessageChannelDestinationResolver(beanFactory));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected final void doSend(MessageChannel destination, Message<?> message) {
|
||||
Assert.notNull(destination, "channel must not be null");
|
||||
long timeout = this.sendTimeout;
|
||||
boolean sent = (timeout >= 0)
|
||||
? destination.send(message, timeout)
|
||||
: destination.send(message);
|
||||
if (!sent) {
|
||||
throw new MessageDeliveryException(message,
|
||||
"failed to send message to channel '" + destination + "' within timeout: " + timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected final <P> Message<P> doReceive(MessageChannel destination) {
|
||||
Assert.state(destination instanceof PollableChannel,
|
||||
"The 'destination' must be a PollableChannel for receive operations.");
|
||||
|
||||
Assert.notNull(destination, "channel must not be null");
|
||||
long timeout = this.receiveTimeout;
|
||||
Message<?> message = (timeout >= 0)
|
||||
? ((PollableChannel) destination).receive(timeout)
|
||||
: ((PollableChannel) destination).receive();
|
||||
if (message == null && this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("failed to receive message from channel '" + destination + "' within timeout: " + timeout);
|
||||
}
|
||||
return (Message<P>) message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final <S, R> Message<R> doSendAndReceive(MessageChannel destination, Message<S> requestMessage) {
|
||||
Object originalReplyChannelHeader = requestMessage.getHeaders().getReplyChannel();
|
||||
Object originalErrorChannelHeader = requestMessage.getHeaders().getErrorChannel();
|
||||
TemporaryReplyChannel replyChannel = new TemporaryReplyChannel(this.receiveTimeout, this.throwExceptionOnLateReply);
|
||||
requestMessage = MessageBuilder.fromMessage(requestMessage)
|
||||
.setReplyChannel(replyChannel)
|
||||
.setErrorChannel(replyChannel)
|
||||
.build();
|
||||
try {
|
||||
this.doSend(destination, requestMessage);
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
replyChannel.setClientWontReceive(true);
|
||||
throw e;
|
||||
}
|
||||
Message<R> reply = this.doReceive(replyChannel);
|
||||
if (reply != null) {
|
||||
reply = MessageBuilder.fromMessage(reply)
|
||||
.setHeader(MessageHeaders.REPLY_CHANNEL, originalReplyChannelHeader)
|
||||
.setHeader(MessageHeaders.ERROR_CHANNEL, originalErrorChannelHeader)
|
||||
.build();
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
|
||||
private static class TemporaryReplyChannel implements PollableChannel {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(TemporaryReplyChannel.class);
|
||||
|
||||
private volatile Message<?> message;
|
||||
|
||||
private final long receiveTimeout;
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
private final boolean throwExceptionOnLateReply;
|
||||
|
||||
private volatile boolean clientTimedOut;
|
||||
|
||||
private volatile boolean clientWontReceive;
|
||||
|
||||
private volatile boolean clientHasReceived;
|
||||
|
||||
|
||||
public TemporaryReplyChannel(long receiveTimeout, boolean throwExceptionOnLateReply) {
|
||||
this.receiveTimeout = receiveTimeout;
|
||||
this.throwExceptionOnLateReply = throwExceptionOnLateReply;
|
||||
}
|
||||
|
||||
public void setClientWontReceive(boolean clientWontReceive) {
|
||||
this.clientWontReceive = clientWontReceive;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Message<?> receive() {
|
||||
return this.receive(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> receive(long timeout) {
|
||||
try {
|
||||
if (this.receiveTimeout < 0) {
|
||||
this.latch.await();
|
||||
this.clientHasReceived = true;
|
||||
}
|
||||
else {
|
||||
if (this.latch.await(this.receiveTimeout, TimeUnit.MILLISECONDS)) {
|
||||
this.clientHasReceived = true;
|
||||
}
|
||||
else {
|
||||
this.clientTimedOut = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return this.message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message) {
|
||||
return this.send(message, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message, long timeout) {
|
||||
this.message = message;
|
||||
this.latch.countDown();
|
||||
if (this.clientTimedOut || this.clientHasReceived || this.clientWontReceive) {
|
||||
String exceptionMessage = "";
|
||||
if (this.clientTimedOut) {
|
||||
exceptionMessage = "Reply message being sent, but the receiving thread has already timed out";
|
||||
}
|
||||
else if (this.clientHasReceived) {
|
||||
exceptionMessage = "Reply message being sent, but the receiving thread has already received a reply";
|
||||
}
|
||||
else if (this.clientWontReceive) {
|
||||
exceptionMessage = "Reply message being sent, but the receiving thread has already caught an exception and won't receive";
|
||||
}
|
||||
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn(exceptionMessage + ":" + message);
|
||||
}
|
||||
if (this.throwExceptionOnLateReply) {
|
||||
throw new MessageDeliveryException(message, exceptionMessage);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* To be used with MessagingTemplate's send method that converts an object to a message.
|
||||
* It allows for further modification of the message after it has been processed
|
||||
* by the converter.
|
||||
*
|
||||
* <p>This is often implemented as an anonymous class within a method implementation.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessagePostProcessor {
|
||||
|
||||
/**
|
||||
* Apply a MessagePostProcessor to the message. The returned message is
|
||||
* typically a modified version of the original.
|
||||
* @param message the message returned from the MessageConverter
|
||||
* @return the modified version of the Message
|
||||
*/
|
||||
Message<?> postProcessMessage(Message<?> message);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageReceivingOperations<D> {
|
||||
|
||||
<P> Message<P> receive() throws MessagingException;
|
||||
|
||||
<P> Message<P> receive(D destination) throws MessagingException;
|
||||
|
||||
Object receiveAndConvert() throws MessagingException;
|
||||
|
||||
Object receiveAndConvert(D destination) throws MessagingException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageRequestReplyOperations<D> {
|
||||
|
||||
Message<?> sendAndReceive(Message<?> requestMessage);
|
||||
|
||||
Message<?> sendAndReceive(D destination, Message<?> requestMessage);
|
||||
|
||||
Object convertSendAndReceive(Object request);
|
||||
|
||||
Object convertSendAndReceive(D destination, Object request);
|
||||
|
||||
Object convertSendAndReceive(Object request, MessagePostProcessor requestPostProcessor);
|
||||
|
||||
Object convertSendAndReceive(D destination, Object request, MessagePostProcessor requestPostProcessor);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.messaging.core;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageSendingOperations<D> {
|
||||
|
||||
<P> void send(Message<P> message) throws MessagingException;
|
||||
|
||||
<P> void send(D destination, Message<P> message) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(T message) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(D destination, T message) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(T message, MessagePostProcessor postProcessor) throws MessagingException;
|
||||
|
||||
<T> void convertAndSend(D destination, T message, MessagePostProcessor postProcessor) throws MessagingException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides core messaging classes.
|
||||
*/
|
||||
package org.springframework.messaging.core;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.messaging.handler.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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.messaging.handler.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 {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.messaging.handler.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 MessageMapping {
|
||||
|
||||
/**
|
||||
* Destination values for the message.
|
||||
*/
|
||||
String[] value() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Annotations and support classes for handling messages.
|
||||
*/
|
||||
package org.springframework.messaging.handler.annotation;
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.messaging.handler.annotation.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.annotation.MessageBody;
|
||||
import org.springframework.messaging.handler.method.MessageArgumentResolver;
|
||||
import org.springframework.messaging.support.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class MessageBodyArgumentResolver implements MessageArgumentResolver {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.messaging.handler.annotation.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Support classes for working with annotated message-handling methods.
|
||||
*/
|
||||
package org.springframework.messaging.handler.annotation.support;
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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.messaging.handler.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.method.HandlerMethod;
|
||||
|
||||
/**
|
||||
* Invokes the handler method for a given message after resolving
|
||||
* its method argument values through registered {@link MessageArgumentResolver}s.
|
||||
* <p>
|
||||
* Use {@link #setMessageMethodArgumentResolvers(MessageArgumentResolverComposite)}
|
||||
* to customize the list of argument resolvers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class InvocableMessageHandlerMethod extends HandlerMethod {
|
||||
|
||||
private MessageArgumentResolverComposite argumentResolvers = new MessageArgumentResolverComposite();
|
||||
|
||||
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 MessageArgumentResolver}s to use to use for resolving method
|
||||
* argument values.
|
||||
*/
|
||||
public void setMessageMethodArgumentResolvers(MessageArgumentResolverComposite 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.messaging.handler.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 {@link Message}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageArgumentResolver {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.messaging.handler.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 MessageArgumentResolver}. Previously resolved method parameters are cached
|
||||
* for faster lookups.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class MessageArgumentResolverComposite implements MessageArgumentResolver {
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final List<MessageArgumentResolver> argumentResolvers = new LinkedList<MessageArgumentResolver>();
|
||||
|
||||
private final Map<MethodParameter, MessageArgumentResolver> argumentResolverCache =
|
||||
new ConcurrentHashMap<MethodParameter, MessageArgumentResolver>(256);
|
||||
|
||||
|
||||
/**
|
||||
* Return a read-only list with the contained resolvers, or an empty list.
|
||||
*/
|
||||
public List<MessageArgumentResolver> getResolvers() {
|
||||
return Collections.unmodifiableList(this.argumentResolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given {@linkplain MethodParameter method parameter} is supported by any registered
|
||||
* {@link MessageArgumentResolver}.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return getArgumentResolver(parameter) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over registered {@link MessageArgumentResolver}s and invoke the one that supports it.
|
||||
* @exception IllegalStateException if no suitable {@link MessageArgumentResolver} is found.
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
|
||||
MessageArgumentResolver resolver = getArgumentResolver(parameter);
|
||||
Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
|
||||
return resolver.resolveArgument(parameter, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a registered {@link MessageArgumentResolver} that supports the given method parameter.
|
||||
*/
|
||||
private MessageArgumentResolver getArgumentResolver(MethodParameter parameter) {
|
||||
MessageArgumentResolver result = this.argumentResolverCache.get(parameter);
|
||||
if (result == null) {
|
||||
for (MessageArgumentResolver resolver : this.argumentResolvers) {
|
||||
if (resolver.supportsParameter(parameter)) {
|
||||
result = resolver;
|
||||
this.argumentResolverCache.put(parameter, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link MessageArgumentResolver}.
|
||||
*/
|
||||
public MessageArgumentResolverComposite addResolver(MessageArgumentResolver argumentResolver) {
|
||||
this.argumentResolvers.add(argumentResolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link MessageArgumentResolver}s.
|
||||
*/
|
||||
public MessageArgumentResolverComposite addResolvers(List<? extends MessageArgumentResolver> argumentResolvers) {
|
||||
if (argumentResolvers != null) {
|
||||
for (MessageArgumentResolver resolver : argumentResolvers) {
|
||||
this.argumentResolvers.add(resolver);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.messaging.handler.method;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* Strategy interface to handle the value returned from the invocation of a
|
||||
* method handling a {@link Message}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageReturnValueHandler {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.messaging.handler.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 MessageReturnValueHandlerComposite implements MessageReturnValueHandler {
|
||||
|
||||
private final List<MessageReturnValueHandler> returnValueHandlers = new ArrayList<MessageReturnValueHandler>();
|
||||
|
||||
|
||||
/**
|
||||
* Add the given {@link MessageReturnValueHandler}.
|
||||
*/
|
||||
public MessageReturnValueHandlerComposite addHandler(MessageReturnValueHandler returnValuehandler) {
|
||||
this.returnValueHandlers.add(returnValuehandler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given {@link MessageReturnValueHandler}s.
|
||||
*/
|
||||
public MessageReturnValueHandlerComposite addHandlers(List<? extends MessageReturnValueHandler> handlers) {
|
||||
if (handlers != null) {
|
||||
for (MessageReturnValueHandler handler : handlers) {
|
||||
this.returnValueHandlers.add(handler);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsReturnType(MethodParameter returnType) {
|
||||
return getReturnValueHandler(returnType) != null;
|
||||
}
|
||||
|
||||
private MessageReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
|
||||
for (MessageReturnValueHandler handler : this.returnValueHandlers) {
|
||||
if (handler.supportsReturnType(returnType)) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReturnValue(Object returnValue, MethodParameter returnType, Message<?> message)
|
||||
throws Exception {
|
||||
|
||||
MessageReturnValueHandler handler = getReturnValueHandler(returnType);
|
||||
Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]");
|
||||
handler.handleReturnValue(returnValue, returnType, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Abstractions and classes for working with message-handling methods.
|
||||
*/
|
||||
package org.springframework.messaging.handler.method;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Support for working with messaging APIs and protocols.
|
||||
*/
|
||||
package org.springframework.messaging;
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.messaging.simp;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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.messaging.simp;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* A base class for working with message headers in simple messaging protocols that
|
||||
* support basic messaging patterns. Provides uniform access to specific values common
|
||||
* across protocols such as a destination, message type (e.g. publish, subscribe, etc),
|
||||
* 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 SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor {
|
||||
|
||||
public static final String DESTINATIONS = "destinations";
|
||||
|
||||
// TODO
|
||||
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 SimpMessageHeaderAccessor(SimpMessageType 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 SimpMessageHeaderAccessor(Message<?> message) {
|
||||
super(message);
|
||||
Assert.notNull(message, "message is required");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create {@link SimpMessageHeaderAccessor} for a new {@link Message} with
|
||||
* {@link SimpMessageType#MESSAGE}.
|
||||
*/
|
||||
public static SimpMessageHeaderAccessor create() {
|
||||
return new SimpMessageHeaderAccessor(SimpMessageType.MESSAGE, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link SimpMessageHeaderAccessor} for a new {@link Message} of a specific type.
|
||||
*/
|
||||
public static SimpMessageHeaderAccessor create(SimpMessageType messageType) {
|
||||
return new SimpMessageHeaderAccessor(messageType, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link SimpMessageHeaderAccessor} from the headers of an existing message.
|
||||
*/
|
||||
public static SimpMessageHeaderAccessor wrap(Message<?> message) {
|
||||
return new SimpMessageHeaderAccessor(message);
|
||||
}
|
||||
|
||||
|
||||
public SimpMessageType getMessageType() {
|
||||
return (SimpMessageType) 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.messaging.simp;
|
||||
|
||||
|
||||
/**
|
||||
* A generic representation of different kinds of messages found in simple messaging
|
||||
* protocols like STOMP.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public enum SimpMessageType {
|
||||
|
||||
CONNECT,
|
||||
|
||||
MESSAGE,
|
||||
|
||||
SUBSCRIBE,
|
||||
|
||||
UNSUBSCRIBE,
|
||||
|
||||
DISCONNECT,
|
||||
|
||||
OTHER;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.messaging.simp;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* A specialization of {@link AbstractMessageSendingTemplate} that adds String-based
|
||||
* destinations as a message header.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SimpMessagingTemplate extends AbstractMessageSendingTemplate<String> {
|
||||
|
||||
private final MessageChannel outputChannel;
|
||||
|
||||
private volatile long sendTimeout = -1;
|
||||
|
||||
|
||||
public SimpMessagingTemplate(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");
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
headers.copyHeaders(message.getHeaders());
|
||||
headers.setDestination(destination);
|
||||
message = MessageBuilder.withPayload(message.getPayload()).copyHeaders(headers.toMap()).build();
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.messaging.simp.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 {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.messaging.simp.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 {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.messaging.simp.annotation.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.handler.method.MessageReturnValueHandler;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.messaging.support.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class MessageSendingReturnValueHandler implements MessageReturnValueHandler {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SimpMessageHeaderAccessor inputHeaders = SimpMessageHeaderAccessor.wrap(message);
|
||||
Message<?> returnMessage = (returnValue instanceof Message) ? (Message<?>) returnValue : null;
|
||||
Object returnPayload = (returnMessage != null) ? returnMessage.getPayload() : returnValue;
|
||||
|
||||
SimpMessageHeaderAccessor returnHeaders = (returnMessage != null) ?
|
||||
SimpMessageHeaderAccessor.wrap(returnMessage) : SimpMessageHeaderAccessor.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
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.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractSimpMessageHandler 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<SimpMessageType> getSupportedMessageTypes();
|
||||
|
||||
|
||||
protected boolean canHandle(Message<?> message, SimpMessageType messageType) {
|
||||
|
||||
if (!CollectionUtils.isEmpty(getSupportedMessageTypes())) {
|
||||
if (!getSupportedMessageTypes().contains(messageType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isDestinationAllowed(message);
|
||||
}
|
||||
|
||||
protected boolean isDestinationAllowed(Message<?> message) {
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.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 {
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
SimpMessageType messageType = headers.getMessageType();
|
||||
|
||||
if (!canHandle(message, messageType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SimpMessageType.MESSAGE.equals(messageType)) {
|
||||
handlePublish(message);
|
||||
}
|
||||
else if (SimpMessageType.SUBSCRIBE.equals(messageType)) {
|
||||
handleSubscribe(message);
|
||||
}
|
||||
else if (SimpMessageType.UNSUBSCRIBE.equals(messageType)) {
|
||||
handleUnsubscribe(message);
|
||||
}
|
||||
else if (SimpMessageType.CONNECT.equals(messageType)) {
|
||||
handleConnect(message);
|
||||
}
|
||||
else if (SimpMessageType.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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
if (!SimpMessageType.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) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
if (!SimpMessageType.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) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message);
|
||||
if (!SimpMessageType.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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
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.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.support.MessageBodyArgumentResolver;
|
||||
import org.springframework.messaging.handler.annotation.support.MessageExceptionHandlerMethodResolver;
|
||||
import org.springframework.messaging.handler.method.MessageArgumentResolverComposite;
|
||||
import org.springframework.messaging.handler.method.InvocableMessageHandlerMethod;
|
||||
import org.springframework.messaging.handler.method.MessageReturnValueHandlerComposite;
|
||||
import org.springframework.messaging.simp.annotation.SubscribeEvent;
|
||||
import org.springframework.messaging.simp.annotation.UnsubscribeEvent;
|
||||
import org.springframework.messaging.simp.annotation.support.MessageSendingReturnValueHandler;
|
||||
import org.springframework.messaging.simp.MessageHolder;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.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.method.HandlerMethod;
|
||||
import org.springframework.web.method.HandlerMethodSelector;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class AnnotationSimpMessageHandler extends AbstractSimpMessageHandler
|
||||
implements ApplicationContextAware, InitializingBean {
|
||||
|
||||
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 MessageArgumentResolverComposite argumentResolvers = new MessageArgumentResolverComposite();
|
||||
|
||||
private MessageReturnValueHandlerComposite returnValueHandlers = new MessageReturnValueHandlerComposite();
|
||||
|
||||
|
||||
/**
|
||||
* @param inboundChannel a channel for processing incoming messages from clients
|
||||
* @param outboundChannel a channel for messages going out to clients
|
||||
*/
|
||||
public AnnotationSimpMessageHandler(MessageChannel outboundChannel) {
|
||||
Assert.notNull(outboundChannel, "outboundChannel is required");
|
||||
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<SimpMessageType> getSupportedMessageTypes() {
|
||||
return Arrays.asList(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE, SimpMessageType.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) {
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SimpleBrokerMessageHandler extends AbstractSimpMessageHandler {
|
||||
|
||||
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 SimpleBrokerMessageHandler(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<SimpMessageType> getSupportedMessageTypes() {
|
||||
return Arrays.asList(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE, SimpMessageType.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.isTraceEnabled()) {
|
||||
logger.trace("Message received: " + message);
|
||||
}
|
||||
|
||||
String destination = SimpMessageHeaderAccessor.wrap(message).getDestination();
|
||||
|
||||
MultiValueMap<String,String> subscriptions = this.subscriptionRegistry.findSubscriptions(message);
|
||||
|
||||
for (String sessionId : subscriptions.keySet()) {
|
||||
for (String subscriptionId : subscriptions.get(sessionId)) {
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.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 = SimpMessageHeaderAccessor.wrap(message).getSessionId();
|
||||
this.subscriptionRegistry.removeSessionSubscriptions(sessionId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.messaging.simp.handler;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Generic support for simple messaging protocols (like STOMP).
|
||||
*/
|
||||
package org.springframework.messaging.simp;
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @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, SimpMessageType> commandToMessageType = new HashMap<StompCommand, SimpMessageType>();
|
||||
|
||||
static {
|
||||
commandToMessageType.put(StompCommand.CONNECT, SimpMessageType.CONNECT);
|
||||
commandToMessageType.put(StompCommand.STOMP, SimpMessageType.CONNECT);
|
||||
commandToMessageType.put(StompCommand.SEND, SimpMessageType.MESSAGE);
|
||||
commandToMessageType.put(StompCommand.MESSAGE, SimpMessageType.MESSAGE);
|
||||
commandToMessageType.put(StompCommand.SUBSCRIBE, SimpMessageType.SUBSCRIBE);
|
||||
commandToMessageType.put(StompCommand.UNSUBSCRIBE, SimpMessageType.UNSUBSCRIBE);
|
||||
commandToMessageType.put(StompCommand.DISCONNECT, SimpMessageType.DISCONNECT);
|
||||
}
|
||||
|
||||
public SimpMessageType getMessageType() {
|
||||
SimpMessageType messageType = commandToMessageType.get(this);
|
||||
return (messageType != null) ? messageType : SimpMessageType.OTHER;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.messaging.simp.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
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.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
/**
|
||||
* 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 SimpMessageHeaderAccessor {
|
||||
|
||||
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);
|
||||
initSimpMessageHeaders();
|
||||
}
|
||||
|
||||
private void initSimpMessageHeaders() {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
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.simp.handler.AbstractSimpMessageHandler;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
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 StompRelayMessageHandler extends AbstractSimpMessageHandler 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 StompRelayMessageHandler(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<SimpMessageType> 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(SimpMessageHeaderAccessor.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
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.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
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);
|
||||
SimpMessageType messageType = stompHeaders.getMessageType();
|
||||
if (SimpMessageType.CONNECT.equals(messageType)) {
|
||||
handleConnect(session, message);
|
||||
}
|
||||
else if (SimpMessageType.MESSAGE.equals(messageType)) {
|
||||
handlePublish(message);
|
||||
}
|
||||
else if (SimpMessageType.SUBSCRIBE.equals(messageType)) {
|
||||
handleSubscribe(message);
|
||||
}
|
||||
else if (SimpMessageType.UNSUBSCRIBE.equals(messageType)) {
|
||||
handleUnsubscribe(message);
|
||||
}
|
||||
else if (SimpMessageType.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());
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.messaging.support;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A message implementation that accepts a {@link Throwable} payload.
|
||||
* Once created this object is immutable.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 4.0
|
||||
* @see MessageBuilder
|
||||
*/
|
||||
public class ErrorMessage extends GenericMessage<Throwable> {
|
||||
|
||||
private static final long serialVersionUID = -5470210965279837728L;
|
||||
|
||||
|
||||
public ErrorMessage(Throwable payload) {
|
||||
super(payload);
|
||||
}
|
||||
|
||||
public ErrorMessage(Throwable payload, Map<String, Object> headers) {
|
||||
super(payload, headers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.messaging.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Base Message class defining common properties such as id, payload, and headers.
|
||||
* Once created this object is immutable.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
* @see MessageBuilder
|
||||
*/
|
||||
public class GenericMessage<T> implements Message<T>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = -9004496725833093406L;
|
||||
|
||||
|
||||
private final T payload;
|
||||
|
||||
private final MessageHeaders headers;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new message with the given payload.
|
||||
*
|
||||
* @param payload the message payload
|
||||
*/
|
||||
protected GenericMessage(T payload) {
|
||||
this(payload, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new message with the given payload. The provided map
|
||||
* will be used to populate the message headers
|
||||
*
|
||||
* @param payload the message payload
|
||||
* @param headers message headers
|
||||
* @see MessageHeaders
|
||||
*/
|
||||
protected GenericMessage(T payload, Map<String, Object> headers) {
|
||||
Assert.notNull(payload, "payload must not be null");
|
||||
if (headers == null) {
|
||||
headers = new HashMap<String, Object>();
|
||||
}
|
||||
else {
|
||||
headers = new HashMap<String, Object>(headers);
|
||||
}
|
||||
this.headers = new MessageHeaders(headers);
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
|
||||
public MessageHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
public T getPayload() {
|
||||
return this.payload;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "[Payload=" + this.payload + "][Headers=" + this.headers + "]";
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return this.headers.hashCode() * 23 + ObjectUtils.nullSafeHashCode(this.payload);
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj != null && obj instanceof GenericMessage<?>) {
|
||||
GenericMessage<?> other = (GenericMessage<?>) obj;
|
||||
if (!this.headers.getId().equals(other.headers.getId())) {
|
||||
return false;
|
||||
}
|
||||
return this.headers.equals(other.headers)
|
||||
&& this.payload.equals(other.payload);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.messaging.support;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A builder for creating {@link GenericMessage} or {@link ErrorMessage} if the payload is
|
||||
* {@link Throwable}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
* @see GenericMessage
|
||||
* @see ErrorMessage
|
||||
*/
|
||||
public final class MessageBuilder<T> {
|
||||
|
||||
private final T payload;
|
||||
|
||||
private final MessageHeaderAccessor headerAccessor;
|
||||
|
||||
private final Message<T> originalMessage;
|
||||
|
||||
|
||||
/**
|
||||
* Private constructor to be invoked from the static factory methods only.
|
||||
*/
|
||||
private MessageBuilder(T payload, Message<T> originalMessage) {
|
||||
Assert.notNull(payload, "payload must not be null");
|
||||
this.payload = payload;
|
||||
this.originalMessage = originalMessage;
|
||||
this.headerAccessor = new MessageHeaderAccessor(originalMessage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a builder for a new {@link Message} instance pre-populated with all of the
|
||||
* headers copied from the provided message. The payload of the provided Message will
|
||||
* also be used as the payload for the new message.
|
||||
*
|
||||
* @param message the Message from which the payload and all headers will be copied
|
||||
*/
|
||||
public static <T> MessageBuilder<T> fromMessage(Message<T> message) {
|
||||
Assert.notNull(message, "message must not be null");
|
||||
MessageBuilder<T> builder = new MessageBuilder<T>(message.getPayload(), message);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder for a new {@link Message} instance with the provided payload.
|
||||
*
|
||||
* @param payload the payload for the new message
|
||||
*/
|
||||
public static <T> MessageBuilder<T> withPayload(T payload) {
|
||||
MessageBuilder<T> builder = new MessageBuilder<T>(payload, null);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name. If the provided value is {@code null},
|
||||
* the header will be removed.
|
||||
*/
|
||||
public MessageBuilder<T> setHeader(String headerName, Object headerValue) {
|
||||
this.headerAccessor.setHeader(headerName, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name only if the header name is not already
|
||||
* associated with a value.
|
||||
*/
|
||||
public MessageBuilder<T> setHeaderIfAbsent(String headerName, Object headerValue) {
|
||||
this.headerAccessor.setHeaderIfAbsent(headerName, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all headers provided via array of 'headerPatterns'. As the name suggests
|
||||
* the array may contain simple matching patterns for header names. Supported pattern
|
||||
* styles are: "xxx*", "*xxx", "*xxx*" and "xxx*yyy".
|
||||
*/
|
||||
public MessageBuilder<T> removeHeaders(String... headerPatterns) {
|
||||
this.headerAccessor.removeHeaders(headerPatterns);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Remove the value for the given header name.
|
||||
*/
|
||||
public MessageBuilder<T> removeHeader(String headerName) {
|
||||
this.headerAccessor.removeHeader(headerName);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the name-value pairs from the provided Map. This operation will overwrite any
|
||||
* existing values. Use { {@link #copyHeadersIfAbsent(Map)} to avoid overwriting
|
||||
* values. Note that the 'id' and 'timestamp' header values will never be overwritten.
|
||||
*/
|
||||
public MessageBuilder<T> copyHeaders(Map<String, ?> headersToCopy) {
|
||||
this.headerAccessor.copyHeaders(headersToCopy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the name-value pairs from the provided Map. This operation will <em>not</em>
|
||||
* overwrite any existing values.
|
||||
*/
|
||||
public MessageBuilder<T> copyHeadersIfAbsent(Map<String, ?> headersToCopy) {
|
||||
this.headerAccessor.copyHeadersIfAbsent(headersToCopy);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageBuilder<T> setReplyChannel(MessageChannel replyChannel) {
|
||||
this.headerAccessor.setReplyChannel(replyChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageBuilder<T> setReplyChannelName(String replyChannelName) {
|
||||
this.headerAccessor.setReplyChannelName(replyChannelName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageBuilder<T> setErrorChannel(MessageChannel errorChannel) {
|
||||
this.headerAccessor.setErrorChannel(errorChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MessageBuilder<T> setErrorChannelName(String errorChannelName) {
|
||||
this.headerAccessor.setErrorChannelName(errorChannelName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Message<T> build() {
|
||||
if ((this.originalMessage != null) && !this.headerAccessor.isModified()) {
|
||||
return this.originalMessage;
|
||||
}
|
||||
if (this.payload instanceof Throwable) {
|
||||
return (Message<T>) new ErrorMessage((Throwable) this.payload, this.headerAccessor.toMap());
|
||||
}
|
||||
return new GenericMessage<T>(this.payload, this.headerAccessor.toMap());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.messaging.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
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.MessageHeaders;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A base class for read/write access to {@link MessageHeaders}. Supports creation of new
|
||||
* headers or modification of existing message headers.
|
||||
*
|
||||
* <p>Sub-classes can provide additional typed getters and setters for convenient access
|
||||
* to specific headers. Getters and setters should delegate to {@link #getHeader(String)}
|
||||
* or {@link #setHeader(String, Object)} respectively. At the end {@link #toMap()} can be
|
||||
* used to obtain the resulting headers.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class MessageHeaderAccessor {
|
||||
|
||||
protected Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
// wrapped read-only message headers
|
||||
private final MessageHeaders originalHeaders;
|
||||
|
||||
// header updates
|
||||
private final Map<String, Object> headers = new HashMap<String, Object>(4);
|
||||
|
||||
|
||||
/**
|
||||
* A constructor for creating new message headers.
|
||||
*/
|
||||
public MessageHeaderAccessor() {
|
||||
this.originalHeaders = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor for accessing and modifying existing message headers.
|
||||
*/
|
||||
public MessageHeaderAccessor(Message<?> message) {
|
||||
this.originalHeaders = (message != null) ? message.getHeaders() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a header map including original, wrapped headers (if any) plus additional
|
||||
* header updates made through accessor methods.
|
||||
*/
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> result = new HashMap<String, Object>();
|
||||
if (this.originalHeaders != null) {
|
||||
result.putAll(this.originalHeaders);
|
||||
}
|
||||
for (String key : this.headers.keySet()) {
|
||||
Object value = this.headers.get(key);
|
||||
if (value == null) {
|
||||
result.remove(key);
|
||||
}
|
||||
else {
|
||||
result.put(key, value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return (!this.headers.isEmpty());
|
||||
}
|
||||
|
||||
public Object getHeader(String headerName) {
|
||||
if (this.headers.containsKey(headerName)) {
|
||||
return this.headers.get(headerName);
|
||||
}
|
||||
else if (this.originalHeaders != null) {
|
||||
return this.originalHeaders.get(headerName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name. If the provided value is {@code null} the
|
||||
* header will be removed.
|
||||
*/
|
||||
public void setHeader(String name, Object value) {
|
||||
Assert.isTrue(!isReadOnly(name), "The '" + name + "' header is read-only.");
|
||||
verifyType(name, value);
|
||||
if (!ObjectUtils.nullSafeEquals(value, getHeader(name))) {
|
||||
this.headers.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isReadOnly(String headerName) {
|
||||
return MessageHeaders.ID.equals(headerName) || MessageHeaders.TIMESTAMP.equals(headerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name only if the header name is not already associated with a value.
|
||||
*/
|
||||
public void setHeaderIfAbsent(String name, Object value) {
|
||||
if (getHeader(name) == null) {
|
||||
setHeader(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all headers provided via array of 'headerPatterns'. As the name suggests
|
||||
* the array may contain simple matching patterns for header names. Supported pattern
|
||||
* styles are: "xxx*", "*xxx", "*xxx*" and "xxx*yyy".
|
||||
*/
|
||||
public void removeHeaders(String... headerPatterns) {
|
||||
List<String> headersToRemove = new ArrayList<String>();
|
||||
for (String pattern : headerPatterns) {
|
||||
if (StringUtils.hasLength(pattern)){
|
||||
if (pattern.contains("*")){
|
||||
headersToRemove.addAll(getMatchingHeaderNames(pattern, this.headers));
|
||||
headersToRemove.addAll(getMatchingHeaderNames(pattern, this.originalHeaders));
|
||||
}
|
||||
else {
|
||||
headersToRemove.add(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String headerToRemove : headersToRemove) {
|
||||
removeHeader(headerToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getMatchingHeaderNames(String pattern, Map<String, Object> headers) {
|
||||
List<String> matchingHeaderNames = new ArrayList<String>();
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, Object> header: headers.entrySet()) {
|
||||
if (PatternMatchUtils.simpleMatch(pattern, header.getKey())) {
|
||||
matchingHeaderNames.add(header.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchingHeaderNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the value for the given header name.
|
||||
*/
|
||||
public void removeHeader(String headerName) {
|
||||
if (StringUtils.hasLength(headerName) && !isReadOnly(headerName)) {
|
||||
setHeader(headerName, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the name-value pairs from the provided Map. This operation will overwrite any
|
||||
* existing values. Use { {@link #copyHeadersIfAbsent(Map)} to avoid overwriting
|
||||
* values.
|
||||
*/
|
||||
public void copyHeaders(Map<String, ?> headersToCopy) {
|
||||
Set<String> keys = headersToCopy.keySet();
|
||||
for (String key : keys) {
|
||||
if (!isReadOnly(key)) {
|
||||
setHeader(key, headersToCopy.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the name-value pairs from the provided Map. This operation will <em>not</em>
|
||||
* overwrite any existing values.
|
||||
*/
|
||||
public void copyHeadersIfAbsent(Map<String, ?> headersToCopy) {
|
||||
Set<String> keys = headersToCopy.keySet();
|
||||
for (String key : keys) {
|
||||
if (!this.isReadOnly(key)) {
|
||||
setHeaderIfAbsent(key, headersToCopy.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setReplyChannel(MessageChannel replyChannel) {
|
||||
setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel);
|
||||
}
|
||||
|
||||
public void setReplyChannelName(String replyChannelName) {
|
||||
setHeader(MessageHeaders.REPLY_CHANNEL, replyChannelName);
|
||||
}
|
||||
|
||||
public void setErrorChannel(MessageChannel errorChannel) {
|
||||
setHeader(MessageHeaders.ERROR_CHANNEL, errorChannel);
|
||||
}
|
||||
|
||||
public void setErrorChannelName(String errorChannelName) {
|
||||
setHeader(MessageHeaders.ERROR_CHANNEL, errorChannelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + " [originalHeaders=" + this.originalHeaders
|
||||
+ ", updated headers=" + this.headers + "]";
|
||||
}
|
||||
|
||||
protected void verifyType(String headerName, Object headerValue) {
|
||||
if (headerName != null && headerValue != null) {
|
||||
if (MessageHeaders.ERROR_CHANNEL.equals(headerName)
|
||||
|| MessageHeaders.REPLY_CHANNEL.endsWith(headerName)) {
|
||||
Assert.isTrue(headerValue instanceof MessageChannel || headerValue instanceof String, "The '"
|
||||
+ headerName + "' header value must be a MessageChannel or String.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.messaging.support;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* An extension of {@link MessageHeaderAccessor} that also provides read/write access to
|
||||
* message headers from an external message source. Native message headers are kept
|
||||
* in a {@link MultiValueMap} under the key {@link #NATIVE_HEADERS}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
|
||||
|
||||
public static final String NATIVE_HEADERS = "nativeHeaders";
|
||||
|
||||
|
||||
// wrapped native headers
|
||||
private final Map<String, List<String>> originalNativeHeaders;
|
||||
|
||||
// native header updates
|
||||
private final MultiValueMap<String, String> nativeHeaders = new LinkedMultiValueMap<String, String>(4);
|
||||
|
||||
|
||||
/**
|
||||
* A constructor for creating new headers, accepting an optional native header map.
|
||||
*/
|
||||
public NativeMessageHeaderAccessor(Map<String, List<String>> nativeHeaders) {
|
||||
super();
|
||||
this.originalNativeHeaders = nativeHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor for accessing and modifying existing message headers.
|
||||
*/
|
||||
public NativeMessageHeaderAccessor(Message<?> message) {
|
||||
super(message);
|
||||
this.originalNativeHeaders = initNativeHeaders(message);
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, List<String>> initNativeHeaders(Message<?> message) {
|
||||
if (message != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, List<String>> headers = (Map<String, List<String>>) message.getHeaders().get(NATIVE_HEADERS);
|
||||
if (headers != null) {
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> result = super.toMap();
|
||||
result.put(NATIVE_HEADERS, toNativeHeaderMap());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
return (super.isModified() || (!this.nativeHeaders.isEmpty()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map with native headers including original, wrapped headers (if any) plus
|
||||
* additional header updates made through accessor methods.
|
||||
*/
|
||||
public Map<String, List<String>> toNativeHeaderMap() {
|
||||
Map<String, List<String>> result = new HashMap<String, List<String>>();
|
||||
if (this.originalNativeHeaders != null) {
|
||||
result.putAll(this.originalNativeHeaders);
|
||||
}
|
||||
for (String key : this.nativeHeaders.keySet()) {
|
||||
List<String> value = this.nativeHeaders.get(key);
|
||||
if (value == null) {
|
||||
result.remove(key);
|
||||
}
|
||||
else {
|
||||
result.put(key, value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected List<String> getNativeHeader(String headerName) {
|
||||
if (this.nativeHeaders.containsKey(headerName)) {
|
||||
return this.nativeHeaders.get(headerName);
|
||||
}
|
||||
else if (this.originalNativeHeaders != null) {
|
||||
return this.originalNativeHeaders.get(headerName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String getFirstNativeHeader(String headerName) {
|
||||
List<String> values = getNativeHeader(headerName);
|
||||
return CollectionUtils.isEmpty(values) ? null : values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name. If the provided value is {@code null} the
|
||||
* header will be removed.
|
||||
*/
|
||||
protected void putNativeHeader(String name, List<String> value) {
|
||||
if (!ObjectUtils.nullSafeEquals(value, getHeader(name))) {
|
||||
this.nativeHeaders.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setNativeHeader(String name, String value) {
|
||||
this.nativeHeaders.set(name, value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.messaging.support.channel;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link SubscribableChannel} that sends messages to each of its subscribers.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0
|
||||
*/
|
||||
public class PublishSubscribeChannel implements SubscribableChannel {
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
private final Set<MessageHandler> handlers = new CopyOnWriteArraySet<MessageHandler>();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link PublishSubscribeChannel} instance where messages will be sent
|
||||
* in the callers thread.
|
||||
*/
|
||||
public PublishSubscribeChannel() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link PublishSubscribeChannel} instance where messages will be sent
|
||||
* via the specified executor.
|
||||
* @param executor the executor used to send the message or {@code null} to execute in
|
||||
* the callers thread.
|
||||
*/
|
||||
public PublishSubscribeChannel(Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message) {
|
||||
return send(message, INDEFINITE_TIMEOUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message, long timeout) {
|
||||
Assert.notNull(message, "Message must not be null");
|
||||
Assert.notNull(message.getPayload(), "Message payload must not be null");
|
||||
for (final MessageHandler handler : this.handlers) {
|
||||
dispatchToHandler(message, handler);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void dispatchToHandler(final Message<?> message, final MessageHandler handler) {
|
||||
if (this.executor == null) {
|
||||
handler.handleMessage(message);
|
||||
}
|
||||
else {
|
||||
this.executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handler.handleMessage(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean subscribe(MessageHandler handler) {
|
||||
return this.handlers.add(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unsubscribe(MessageHandler handler) {
|
||||
return this.handlers.remove(handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.messaging.support.channel;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
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.SubscribableChannel;
|
||||
|
||||
import reactor.core.Reactor;
|
||||
import reactor.event.Event;
|
||||
import reactor.event.registry.Registration;
|
||||
import reactor.event.selector.ObjectSelector;
|
||||
import reactor.function.Consumer;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.0
|
||||
*/
|
||||
public class ReactorMessageChannel implements SubscribableChannel {
|
||||
|
||||
private static Log logger = LogFactory.getLog(ReactorMessageChannel.class);
|
||||
|
||||
private final Reactor reactor;
|
||||
|
||||
private final Object key = new Object();
|
||||
|
||||
private String name = toString(); // TODO
|
||||
|
||||
|
||||
private final Map<MessageHandler, Registration<?>> registrations =
|
||||
new HashMap<MessageHandler, Registration<?>>();
|
||||
|
||||
|
||||
public ReactorMessageChannel(Reactor reactor) {
|
||||
this.reactor = reactor;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message) {
|
||||
return send(message, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean send(Message<?> message, long timeout) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Channel " + getName() + ", sending message id=" + message.getHeaders().getId());
|
||||
}
|
||||
this.reactor.notify(this.key, Event.wrap(message));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean subscribe(final MessageHandler handler) {
|
||||
|
||||
if (this.registrations.containsKey(handler)) {
|
||||
logger.warn("Channel " + getName() + ", handler already subscribed " + handler);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Channel " + getName() + ", subscribing handler " + handler);
|
||||
}
|
||||
|
||||
Registration<Consumer<Event<Message<?>>>> registration = this.reactor.on(
|
||||
ObjectSelector.objectSelector(key), new MessageHandlerConsumer(handler));
|
||||
|
||||
this.registrations.put(handler, registration);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unsubscribe(MessageHandler handler) {
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Channel " + getName() + ", removing subscription for handler " + handler);
|
||||
}
|
||||
|
||||
Registration<?> registration = this.registrations.remove(handler);
|
||||
if (registration == null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Channel " + getName() + ", no subscription for handler " + handler);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
registration.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static final class MessageHandlerConsumer implements Consumer<Event<Message<?>>> {
|
||||
|
||||
private final MessageHandler handler;
|
||||
|
||||
private MessageHandlerConsumer(MessageHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Event<Message<?>> event) {
|
||||
Message<?> message = event.getData();
|
||||
try {
|
||||
this.handler.handleMessage(message);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
// TODO
|
||||
logger.error("Failed to process message " + message, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides classes representing various channel types.
|
||||
*/
|
||||
package org.springframework.messaging.support.channel;
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.messaging.support.converter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
* @sicne 4.0
|
||||
*/
|
||||
public class MappingJackson2MessageConverter implements MessageConverter<Object> {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private Type defaultObjectType = Map.class;
|
||||
|
||||
private Class<?> defaultMessagePayloadClass = byte[].class;
|
||||
|
||||
|
||||
/**
|
||||
* Set the default target Object class to convert to in
|
||||
* {@link #fromMessage(Message, Class)}.
|
||||
*/
|
||||
public void setDefaultObjectClass(Type defaultObjectType) {
|
||||
Assert.notNull(defaultObjectType, "defaultObjectType is required");
|
||||
this.defaultObjectType = defaultObjectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of Message payload to convert to in {@link #toMessage(Object)}.
|
||||
* @param payloadClass either byte[] or String
|
||||
*/
|
||||
public void setDefaultTargetPayloadClass(Class<?> payloadClass) {
|
||||
Assert.isTrue(byte[].class.equals(payloadClass) || String.class.equals(payloadClass),
|
||||
"Payload class must be byte[] or String: " + payloadClass);
|
||||
this.defaultMessagePayloadClass = payloadClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(Message<?> message, Type objectType) {
|
||||
|
||||
JavaType javaType = (objectType != null) ?
|
||||
this.objectMapper.constructType(objectType) :
|
||||
this.objectMapper.constructType(this.defaultObjectType);
|
||||
|
||||
Object payload = message.getPayload();
|
||||
try {
|
||||
if (payload instanceof byte[]) {
|
||||
return this.objectMapper.readValue((byte[]) payload, javaType);
|
||||
}
|
||||
else if (payload instanceof String) {
|
||||
return this.objectMapper.readValue((String) payload, javaType);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Unexpected message payload type: " + payload);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new MessageConversionException("Could not read JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <P> Message<P> toMessage(Object object) {
|
||||
P payload;
|
||||
try {
|
||||
if (byte[].class.equals(this.defaultMessagePayloadClass)) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
this.objectMapper.writeValue(out, object);
|
||||
payload = (P) out.toByteArray();
|
||||
}
|
||||
else if (String.class.equals(this.defaultMessagePayloadClass)) {
|
||||
Writer writer = new StringWriter();
|
||||
this.objectMapper.writeValue(writer, object);
|
||||
payload = (P) writer.toString();
|
||||
}
|
||||
else {
|
||||
// Should never happen..
|
||||
throw new IllegalStateException("Unexpected payload class: " + defaultMessagePayloadClass);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
return MessageBuilder.withPayload(payload).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.messaging.support.converter;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MessageConversionException extends MessagingException {
|
||||
|
||||
public MessageConversionException(String description, Throwable cause) {
|
||||
super(description, cause);
|
||||
}
|
||||
|
||||
public MessageConversionException(Message<?> failedMessage, String description, Throwable cause) {
|
||||
super(failedMessage, description, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.messaging.support.converter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public interface MessageConverter<T> {
|
||||
|
||||
<P> Message<P> toMessage(T object);
|
||||
|
||||
T fromMessage(Message<?> message, Type targetClass);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.messaging.support.converter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @since 4.0
|
||||
*/
|
||||
public class SimplePayloadMessageConverter implements MessageConverter<Object> {
|
||||
|
||||
@Override
|
||||
public Message<Object> toMessage(Object object) {
|
||||
return MessageBuilder.withPayload(object).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(Message<?> message, Type targetClass) {
|
||||
return message.getPayload();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Provides classes supporting message conversion.
|
||||
*/
|
||||
package org.springframework.messaging.support.converter;
|
||||
7
spring-messaging/src/main/java/overview.html
Normal file
7
spring-messaging/src/main/java/overview.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Spring's support for messaging architectures and messaging protocols.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user