From 4867546aec48a49a8bbfd63d86dd32a8ed5eaf83 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 6 Apr 2014 11:06:18 -0400 Subject: [PATCH] Enhance MessageHeaderAccessor support Refine semantics of ID and TIMESTAMP headers provided to protected MessageHeaders constructor. Refactor internal implementation of MessageHeaderAccessor. Support mutating headers from a single thread while a message is being built (e.g. StompDecoder creating message + then adding session id). Improve immutablity in NativeMessageHeaderAccessor and in StompHeaderAccessor. Optimize object creation for initializing messages and subsequent accessing their headers. Introduce MessageHeaderAccessorFactory support to enable applying a common strategies for ID and TIMESTAMP generation to every message. Add MessageBuilder shortcut factory method for creating messages from payload and a full-prepared MessageHeaders instance. Also add equivalent constructors to GenericMessage and ErrorMessage. Issue: SPR-11468 --- .../messaging/MessageHeaders.java | 53 ++- ...faultSimpMessageHeaderAccessorFactory.java | 70 ++++ .../simp/SimpMessageHeaderAccessor.java | 89 ++++- .../SimpMessageHeaderAccessorFactory.java | 48 +++ .../broker/AbstractBrokerMessageHandler.java | 2 +- .../simp/stomp/BufferingStompDecoder.java | 2 +- .../DefaultStompHeaderAccessorFactory.java | 78 ++++ .../messaging/simp/stomp/StompDecoder.java | 103 ++--- .../messaging/simp/stomp/StompEncoder.java | 72 ++-- .../simp/stomp/StompHeaderAccessor.java | 293 ++++++++------ .../stomp/StompHeaderAccessorFactory.java | 55 +++ .../support/AbstractMessageChannel.java | 2 +- .../support/ChannelInterceptorChain.java | 8 +- .../messaging/support/ErrorMessage.java | 20 +- .../messaging/support/GenericMessage.java | 23 +- .../messaging/support/MessageBuilder.java | 74 +++- .../support/MessageHeaderAccessor.java | 362 +++++++++++++++--- .../MessageHeaderAccessorFactorySupport.java | 67 ++++ .../support/NativeMessageHeaderAccessor.java | 170 ++++---- .../messaging/MessageHeadersTests.java | 42 +- .../HeaderMethodArgumentResolverTests.java | 4 +- .../simp/SimpMessagingTemplateTests.java | 5 +- .../stomp/BufferingStompDecoderTests.java | 12 + .../messaging/simp/stomp/StompCodecTests.java | 60 ++- .../simp/stomp/StompHeaderAccessorTests.java | 82 +++- .../support/MessageBuilderTests.java | 73 +++- .../support/MessageHeaderAccessorTests.java | 237 ++++++++++-- .../NativeMessageHeaderAccessorTests.java | 206 +++++++--- .../StompSubProtocolHandlerTests.java | 2 +- 29 files changed, 1825 insertions(+), 489 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/DefaultSimpMessageHeaderAccessorFactory.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessorFactory.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompHeaderAccessorFactory.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorFactory.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessorFactorySupport.java diff --git a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java index 8ea1f69511..552440e2f8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,7 +33,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.AlternativeJdkIdGenerator; -import org.springframework.util.Assert; import org.springframework.util.IdGenerator; /** @@ -70,10 +68,12 @@ import org.springframework.util.IdGenerator; */ public class MessageHeaders implements Map, Serializable { - private static final long serialVersionUID = -4615750558355702881L; + private static final long serialVersionUID = 7035068984263400920L; private static final Log logger = LogFactory.getLog(MessageHeaders.class); + public static final UUID ID_VALUE_NONE = new UUID(0,0); + private static volatile IdGenerator idGenerator = null; private static final IdGenerator defaultIdGenerator = new AlternativeJdkIdGenerator(); @@ -99,29 +99,43 @@ public class MessageHeaders implements Map, Serializable { /** - * Consructs a {@link MessageHeaders} from the headers map; adding (or - * overwriting) the {@link #ID} and {@link #TIMESTAMP} headers. + * Construct a {@link MessageHeaders} with the given headers. An {@link #ID} and + * {@link #TIMESTAMP} headers will also be added, overriding any existing values. + * * @param headers a map with headers to add */ public MessageHeaders(Map headers) { - this(headers, ((idGenerator != null) ? idGenerator : defaultIdGenerator).generateId(), - System.currentTimeMillis()); + this(headers, null, null); } /** - * Constructor allowing a sub-class to access the (mutable) header map as well - * to provide the ID and TIMESTAMP header values. + * Constructor providing control over the ID and TIMESTAMP header values. * * @param headers a map with headers to add - * @param id the value for the {@link #ID} header, never {@code null} - * @param timestamp the value for the {@link #TIMESTAMP} header, - * or {@code null} meaning no timestamp header + * @param id the {@link #ID} header value + * @param timestamp the {@link #TIMESTAMP} header value */ protected MessageHeaders(Map headers, UUID id, Long timestamp) { - Assert.notNull(id, "'id' is required"); + this.headers = (headers != null) ? new HashMap(headers) : new HashMap(); - this.headers.put(ID, id); - if (timestamp != null) { + + if (id == null) { + this.headers.put(ID, getIdGenerator().generateId()); + } + else if (id == ID_VALUE_NONE) { + this.headers.remove(ID); + } + else { + this.headers.put(ID, id); + } + + if (timestamp == null) { + this.headers.put(TIMESTAMP, System.currentTimeMillis()); + } + else if (timestamp < 0) { + this.headers.remove(TIMESTAMP); + } + else { this.headers.put(TIMESTAMP, timestamp); } } @@ -131,6 +145,10 @@ public class MessageHeaders implements Map, Serializable { return this.headers; } + protected static IdGenerator getIdGenerator() { + return ((idGenerator != null) ? idGenerator : defaultIdGenerator); + } + public UUID getId() { return this.get(ID, UUID.class); } @@ -179,10 +197,7 @@ public class MessageHeaders implements Map, Serializable { @Override public String toString() { - Map map = new LinkedHashMap(this.headers); - map.put(ID, map.remove(ID)); // remove and add again at the end - map.put(TIMESTAMP, map.remove(TIMESTAMP)); - return map.toString(); + return this.headers.toString(); } /* diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/DefaultSimpMessageHeaderAccessorFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/DefaultSimpMessageHeaderAccessorFactory.java new file mode 100644 index 0000000000..494771fc5c --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/DefaultSimpMessageHeaderAccessorFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2014 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.MessageHeaders; +import org.springframework.messaging.support.MessageHeaderAccessorFactorySupport; +import org.springframework.util.IdGenerator; + +import java.util.UUID; + +/** + * A default implementation of + * {@link org.springframework.messaging.simp.SimpMessageHeaderAccessorFactory}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class DefaultSimpMessageHeaderAccessorFactory extends MessageHeaderAccessorFactorySupport + implements SimpMessageHeaderAccessorFactory { + + + public DefaultSimpMessageHeaderAccessorFactory() { + super.setIdGenerator(ID_VALUE_NONE_GENERATOR); + } + + + @Override + public SimpMessageHeaderAccessor create() { + SimpMessageHeaderAccessor accessor = new SimpMessageHeaderAccessor(SimpMessageType.MESSAGE, null); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + @Override + public SimpMessageHeaderAccessor create(SimpMessageType messageType) { + SimpMessageHeaderAccessor accessor = new SimpMessageHeaderAccessor(messageType, null); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + @Override + public SimpMessageHeaderAccessor wrap(Message message) { + SimpMessageHeaderAccessor accessor = new SimpMessageHeaderAccessor(message); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + + private static final IdGenerator ID_VALUE_NONE_GENERATOR = new IdGenerator() { + @Override + public UUID generateId() { + return MessageHeaders.ID_VALUE_NONE; + } + }; +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java index 78adf8afbd..891e454108 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.messaging.support.NativeMessageHeaderAccessor; import org.springframework.util.Assert; @@ -38,6 +40,10 @@ import org.springframework.util.Assert; */ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { + private static final SimpMessageHeaderAccessorFactory factory = new DefaultSimpMessageHeaderAccessorFactory(); + + // SiMP header names + public static final String CONNECT_MESSAGE_HEADER = "simpConnectMessage"; public static final String DESTINATION_HEADER = "simpDestination"; @@ -83,25 +89,32 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { /** - * Create {@link SimpMessageHeaderAccessor} for a new {@link Message} with - * {@link SimpMessageType#MESSAGE}. + * Create an instance with + * {@link org.springframework.messaging.simp.SimpMessageType} {@code MESSAGE}. */ public static SimpMessageHeaderAccessor create() { - return new SimpMessageHeaderAccessor(SimpMessageType.MESSAGE, null); + return factory.create(); } /** - * Create {@link SimpMessageHeaderAccessor} for a new {@link Message} of a specific type. + * Create an instance with the given + * {@link org.springframework.messaging.simp.SimpMessageType}. */ public static SimpMessageHeaderAccessor create(SimpMessageType messageType) { - return new SimpMessageHeaderAccessor(messageType, null); + return factory.create(messageType); } /** - * Create {@link SimpMessageHeaderAccessor} from the headers of an existing message. + * Create an instance from the payload and headers of the given Message. */ public static SimpMessageHeaderAccessor wrap(Message message) { - return new SimpMessageHeaderAccessor(message); + return factory.wrap(message); + } + + + @Override + protected MessageHeaderAccessor createAccessor(Message message) { + return factory.wrap(message); } public void setMessageTypeIfNotSet(SimpMessageType messageType) { @@ -117,6 +130,13 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (SimpMessageType) getHeader(MESSAGE_TYPE_HEADER); } + /** + * A static alternative for access to the message type. + */ + public static SimpMessageType getMessageType(Map headers) { + return (SimpMessageType) headers.get(MESSAGE_TYPE_HEADER); + } + public void setDestination(String destination) { Assert.notNull(destination, "Destination must not be null"); setHeader(DESTINATION_HEADER, destination); @@ -129,6 +149,17 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (String) getHeader(DESTINATION_HEADER); } + /** + * A static alternative for access to the destination header. + */ + public static String getDestination(Map headers) { + return (String) headers.get(DESTINATION_HEADER); + } + + public void setSubscriptionId(String subscriptionId) { + setHeader(SUBSCRIPTION_ID_HEADER, subscriptionId); + } + /** * @return the subscription id (if any) of the message */ @@ -136,8 +167,15 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (String) getHeader(SUBSCRIPTION_ID_HEADER); } - public void setSubscriptionId(String subscriptionId) { - setHeader(SUBSCRIPTION_ID_HEADER, subscriptionId); + /** + * A static alternative for access to the subscription id header. + */ + public static String getSubscriptionId(Map headers) { + return (String) headers.get(SUBSCRIPTION_ID_HEADER); + } + + public void setSessionId(String sessionId) { + setHeader(SESSION_ID_HEADER, sessionId); } /** @@ -147,8 +185,18 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (String) getHeader(SESSION_ID_HEADER); } - public void setSessionId(String sessionId) { - setHeader(SESSION_ID_HEADER, sessionId); + /** + * A static alternative for access to the session id header. + */ + public static String getSessionId(Map headers) { + return (String) headers.get(SESSION_ID_HEADER); + } + + /** + * A static alternative for access to the session attributes header. + */ + public void setSessionAttributes(Map attributes) { + setHeader(SESSION_ATTRIBUTES, attributes); } /** @@ -159,8 +207,16 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (Map) getHeader(SESSION_ATTRIBUTES); } - public void setSessionAttributes(Map attributes) { - setHeader(SESSION_ATTRIBUTES, attributes); + /** + * A static alternative for access to the session attributes header. + */ + @SuppressWarnings("unchecked") + public static Map getSessionAttributes(Map headers) { + return (Map) headers.get(SESSION_ATTRIBUTES); + } + + public void setUser(Principal principal) { + setHeader(USER_HEADER, principal); } /** @@ -170,8 +226,11 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { return (Principal) getHeader(USER_HEADER); } - public void setUser(Principal principal) { - setHeader(USER_HEADER, principal); + /** + * A static alternative for access to the user header. + */ + public static Principal getUser(Map headers) { + return (Principal) headers.get(USER_HEADER); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessorFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessorFactory.java new file mode 100644 index 0000000000..3d5acafa77 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessorFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2014 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; + +/** + * A factory for creating pre-configured instances of type + * {@link org.springframework.messaging.simp.SimpMessageHeaderAccessor}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public interface SimpMessageHeaderAccessorFactory { + + /** + * Create an instance with + * {@link org.springframework.messaging.simp.SimpMessageType} {@code MESSAGE}. + */ + SimpMessageHeaderAccessor create(); + + /** + * Create an instance with the given + * {@link org.springframework.messaging.simp.SimpMessageType}. + */ + SimpMessageHeaderAccessor create(SimpMessageType messageType); + + /** + * Create an instance from the payload and headers of the given Message. + */ + SimpMessageHeaderAccessor wrap(Message message); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java index f2a90e713e..9c02aff9af 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java @@ -164,7 +164,7 @@ public abstract class AbstractBrokerMessageHandler public final void handleMessage(Message message) { if (!this.running) { if (logger.isTraceEnabled()) { - logger.trace("Message broker is not running. Ignoring message id=" + message.getHeaders().getId()); + logger.trace("Message broker is not running. Ignoring message=" + message); } return; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java index 6d61c0b40e..ba9afe0079 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java @@ -123,7 +123,7 @@ public class BufferingStompDecoder extends StompDecoder { if (bufferToDecode.hasRemaining()) { this.chunks.add(bufferToDecode); - this.expectedContentLength = getContentLength(headers); + this.expectedContentLength = StompHeaderAccessor.getContentLength(headers); } return messages; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompHeaderAccessorFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompHeaderAccessorFactory.java new file mode 100644 index 0000000000..7a59f09c3c --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/DefaultStompHeaderAccessorFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageHeaderAccessorFactorySupport; +import org.springframework.util.IdGenerator; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * A default implementation of + * {@link org.springframework.messaging.simp.stomp.StompHeaderAccessorFactory}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class DefaultStompHeaderAccessorFactory extends MessageHeaderAccessorFactorySupport + implements StompHeaderAccessorFactory { + + + public DefaultStompHeaderAccessorFactory() { + super.setIdGenerator(ID_VALUE_NONE_GENERATOR); + } + + + @Override + public StompHeaderAccessor create(StompCommand command) { + StompHeaderAccessor accessor = new StompHeaderAccessor(command, null); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + @Override + public StompHeaderAccessor create(StompCommand command, Map> headers) { + StompHeaderAccessor accessor = new StompHeaderAccessor(command, headers); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + @Override + public StompHeaderAccessor createForHeartbeat() { + StompHeaderAccessor accessor = new StompHeaderAccessor(); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + @Override + public StompHeaderAccessor wrap(Message message) { + StompHeaderAccessor accessor = new StompHeaderAccessor(message); + updateMessageHeaderAccessor(accessor); + return accessor; + } + + private static final IdGenerator ID_VALUE_NONE_GENERATOR = new IdGenerator() { + @Override + public UUID generateId() { + return MessageHeaders.ID_VALUE_NONE; + } + }; +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java index 44a083414e..13347e13ef 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java @@ -26,10 +26,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.messaging.Message; -import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; +import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MultiValueMap; /** @@ -47,9 +46,9 @@ import org.springframework.util.MultiValueMap; */ public class StompDecoder { - private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); - private static final byte[] HEARTBEAT_PAYLOAD = new byte[] {'\n'}; + static final byte[] HEARTBEAT_PAYLOAD = new byte[] {'\n'}; private final Log logger = LogFactory.getLog(StompDecoder.class); @@ -65,7 +64,7 @@ public class StompDecoder { * @return the decoded messages or an empty list */ public List> decode(ByteBuffer buffer) { - return decode(buffer, new LinkedMultiValueMap()); + return decode(buffer, null); } /** @@ -79,27 +78,25 @@ public class StompDecoder { *

If the buffer contains one ore more STOMP frames, those are returned and * the buffer reset to point to the beginning of the unused partial content. * - *

The input headers map is used to store successfully parsed headers and - * is cleared after ever successfully read message. So when partial content is - * read the caller can check if a "content-length" header was read, which helps - * to determine how much more content is needed before the next STOMP frame - * can be decoded. + *

The output partialMessageHeaders map is used to store successfully parsed + * headers in case of partial content. The caller can then check if a + * "content-length" header was read, which helps to determine how much more + * content is needed before the next attempt to decode. * * @param buffer The buffer to decode the STOMP frame from - * @param headers an empty map that will contain successfully parsed headers + * @param partialMessageHeaders an empty output map that will store the last + * successfully parsed partialMessageHeaders in case of partial message content * in cases where the partial buffer ended with a partial STOMP frame * * @return decoded messages or an empty list * @throws StompConversionException raised in case of decoding issues */ - public List> decode(ByteBuffer buffer, MultiValueMap headers) { - Assert.notNull(headers, "headers is required"); + public List> decode(ByteBuffer buffer, MultiValueMap partialMessageHeaders) { List> messages = new ArrayList>(); while (buffer.hasRemaining()) { - Message m = decodeMessage(buffer, headers); + Message m = decodeMessage(buffer, partialMessageHeaders); if (m != null) { messages.add(m); - headers.clear(); } else { break; @@ -120,17 +117,25 @@ public class StompDecoder { String command = readCommand(buffer); if (command.length() > 0) { - readHeaders(buffer, headers); - byte[] payload = readPayload(buffer, headers); + StompHeaderAccessor headerAccessor = null; + byte[] payload = null; + + if (buffer.remaining() > 0) { + StompCommand stompCommand = StompCommand.valueOf(command); + headerAccessor = StompHeaderAccessor.create(stompCommand); + + readHeaders(buffer, headerAccessor); + payload = readPayload(buffer, headerAccessor); + } if (payload != null) { - StompCommand stompCommand = StompCommand.valueOf(command); - if ((payload.length > 0) && (!stompCommand.isBodyAllowed())) { - throw new StompConversionException(stompCommand + " shouldn't have but " + - "has a payload with length=" + payload.length + ", headers=" + headers); + if ((payload.length > 0) && (!headerAccessor.getCommand().isBodyAllowed())) { + throw new StompConversionException(headerAccessor.getCommand() + + " shouldn't have a payload: length=" + payload.length + ", headers=" + headers); } - decodedMessage = MessageBuilder.withPayload(payload) - .setHeaders(StompHeaderAccessor.create(stompCommand, headers)).build(); + headerAccessor.updateSimpMessageHeadersFromStompHeaders(); + headerAccessor.setLeaveMutable(true); + decodedMessage = MessageBuilder.createMessage(payload, headerAccessor.getMessageHeaders()); if (logger.isDebugEnabled()) { logger.debug("Decoded " + decodedMessage); } @@ -139,6 +144,14 @@ public class StompDecoder { if (logger.isTraceEnabled()) { logger.trace("Received incomplete frame. Resetting buffer."); } + if (headers != null && headerAccessor != null) { + String name = NativeMessageHeaderAccessor.NATIVE_HEADERS; + @SuppressWarnings("unchecked") + MultiValueMap map = (MultiValueMap) headerAccessor.getHeader(name); + if (map != null) { + headers.putAll(map); + } + } buffer.reset(); } } @@ -146,8 +159,9 @@ public class StompDecoder { if (logger.isTraceEnabled()) { logger.trace("Decoded heartbeat"); } - decodedMessage = MessageBuilder.withPayload(HEARTBEAT_PAYLOAD).setHeaders( - StompHeaderAccessor.create(SimpMessageType.HEARTBEAT)).build(); + StompHeaderAccessor headerAccessor = StompHeaderAccessor.createForHeartbeat(); + headerAccessor.setLeaveMutable(true); + decodedMessage = MessageBuilder.createMessage(HEARTBEAT_PAYLOAD, headerAccessor.getMessageHeaders()); } return decodedMessage; } @@ -173,7 +187,7 @@ public class StompDecoder { return new String(command.toByteArray(), UTF8_CHARSET); } - private void readHeaders(ByteBuffer buffer, MultiValueMap headers) { + private void readHeaders(ByteBuffer buffer, StompHeaderAccessor headerAccessor) { while (true) { ByteArrayOutputStream headerStream = new ByteArrayOutputStream(256); while (buffer.remaining() > 0 && !tryConsumeEndOfLine(buffer)) { @@ -191,7 +205,14 @@ public class StompDecoder { else { String headerName = unescape(header.substring(0, colonIndex)); String headerValue = unescape(header.substring(colonIndex + 1)); - headers.add(headerName, headerValue); + try { + headerAccessor.addNativeHeader(headerName, headerValue); + } + catch (InvalidMimeTypeException ex) { + if (buffer.remaining() > 0) { + throw ex; + } + } } } else { @@ -237,8 +258,17 @@ public class StompDecoder { return sb.toString(); } - private byte[] readPayload(ByteBuffer buffer, MultiValueMap headers) { - Integer contentLength = getContentLength(headers); + private byte[] readPayload(ByteBuffer buffer, StompHeaderAccessor headerAccessor) { + + Integer contentLength; + try { + contentLength = headerAccessor.getContentLength(); + } + catch (NumberFormatException ex) { + logger.warn("Ignoring invalid content-length: '" + headerAccessor); + contentLength = null; + } + if (contentLength != null && contentLength >= 0) { if (buffer.remaining() > contentLength) { byte[] payload = new byte[contentLength]; @@ -267,19 +297,6 @@ public class StompDecoder { return null; } - protected Integer getContentLength(MultiValueMap headers) { - if (headers.containsKey(StompHeaderAccessor.STOMP_CONTENT_LENGTH_HEADER)) { - String rawContentLength = headers.getFirst(StompHeaderAccessor.STOMP_CONTENT_LENGTH_HEADER); - try { - return Integer.valueOf(rawContentLength); - } - catch (NumberFormatException ex) { - logger.warn("Ignoring invalid content-length header value: '" + rawContentLength + "'"); - } - } - return null; - } - /** * Try to read an EOL incrementing the buffer position if successful. * diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java index ae7e54315a..d097d98dee 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java @@ -19,7 +19,7 @@ package org.springframework.messaging.simp.stomp; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.nio.charset.Charset; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -28,7 +28,10 @@ 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.messaging.support.NativeMessageHeaderAccessor; +import org.springframework.util.Assert; /** * An encoder for STOMP frames. @@ -43,32 +46,45 @@ public final class StompEncoder { private static final byte COLON = ':'; - private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); - private final Log logger = LogFactory.getLog(StompEncoder.class); /** * Encodes the given STOMP {@code message} into a {@code byte[]} + * * @param message the message to encode * @return the encoded message */ public byte[] encode(Message message) { + return encode(message.getHeaders(), message.getPayload()); + } + + /** + * Encodes the given payload and headers into a {@code byte[]}. + * + * @param headers the headers + * @param payload the payload + * @return the encoded message + */ + public byte[] encode(Map headers, byte[] payload) { + Assert.notNull(headers, "'headers' is required"); + Assert.notNull(payload, "'payload' is required"); try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(128 + message.getPayload().length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(128 + payload.length); DataOutputStream output = new DataOutputStream(baos); - StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); - if (SimpMessageType.HEARTBEAT == headers.getMessageType()) { + if (SimpMessageType.HEARTBEAT.equals(SimpMessageHeaderAccessor.getMessageType(headers))) { logger.trace("Encoded heartbeat"); - output.write(message.getPayload()); + output.write(StompDecoder.HEARTBEAT_PAYLOAD); } else { - output.write(headers.getCommand().toString().getBytes(UTF8_CHARSET)); + StompCommand command = StompHeaderAccessor.getCommand(headers); + Assert.notNull(command, "Missing STOMP command: " + headers); + output.write(command.toString().getBytes(StompDecoder.UTF8_CHARSET)); output.write(LF); - writeHeaders(headers, message, output); + writeHeaders(command, headers, payload, output); output.write(LF); - writeBody(message, output); + writeBody(payload, output); output.write((byte) 0); } @@ -79,20 +95,30 @@ public final class StompEncoder { } } - private void writeHeaders(StompHeaderAccessor headers, Message message, DataOutputStream output) + private void writeHeaders(StompCommand command, Map headers, byte[] payload, DataOutputStream output) throws IOException { - StompCommand command = headers.getCommand(); - Map> stompHeaders = headers.toStompHeaderMap(); - boolean shouldEscape = (command != StompCommand.CONNECT && command != StompCommand.CONNECTED); + @SuppressWarnings("unchecked") + Map> nativeHeaders = + (Map>) headers.get(NativeMessageHeaderAccessor.NATIVE_HEADERS); if (logger.isDebugEnabled()) { - logger.debug("Encoded STOMP " + command + ", headers=" + stompHeaders); + logger.debug("Encoding STOMP " + command + ", headers=" + nativeHeaders); } - for (Entry> entry : stompHeaders.entrySet()) { + if (nativeHeaders == null) { + return; + } + + boolean shouldEscape = (command != StompCommand.CONNECT && command != StompCommand.CONNECTED); + + for (Entry> entry : nativeHeaders.entrySet()) { byte[] key = encodeHeaderString(entry.getKey(), shouldEscape); - for (String value : entry.getValue()) { + List values = entry.getValue(); + if (StompHeaderAccessor.STOMP_PASSCODE_HEADER.equals(entry.getKey())) { + values = Arrays.asList(StompHeaderAccessor.getPasscode(headers)); + } + for (String value : values) { output.write(key); output.write(COLON); output.write(encodeHeaderString(value, shouldEscape)); @@ -100,16 +126,16 @@ public final class StompEncoder { } } if (command.requiresContentLength()) { - int contentLength = message.getPayload().length; - output.write("content-length:".getBytes(UTF8_CHARSET)); - output.write(Integer.toString(contentLength).getBytes(UTF8_CHARSET)); + int contentLength = payload.length; + output.write("content-length:".getBytes(StompDecoder.UTF8_CHARSET)); + output.write(Integer.toString(contentLength).getBytes(StompDecoder.UTF8_CHARSET)); output.write(LF); } } private byte[] encodeHeaderString(String input, boolean escape) { input = escape ? escape(input) : input; - return input.getBytes(UTF8_CHARSET); + return input.getBytes(StompDecoder.UTF8_CHARSET); } /** @@ -139,8 +165,8 @@ public final class StompEncoder { return sb.toString(); } - private void writeBody(Message message, DataOutputStream output) throws IOException { - output.write(message.getPayload()); + private void writeBody(byte[] payload, DataOutputStream output) throws IOException { + output.write(payload); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java index 5b1d1d0e94..4cc63b7bc3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java @@ -26,27 +26,45 @@ import java.util.concurrent.atomic.AtomicLong; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; +import org.springframework.util.MultiValueMap; 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. + * A {@code MessageHeaderAccessor} to use when creating a {@code Message} from a + * decoded STOMP frame, or when encoding a {@code Message} to a STOMP frame. * - *

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. + *

When created from STOMP frame content, the actual STOMP headers are stored + * in the native header sub-map managed by the parent class + * {@link org.springframework.messaging.support.NativeMessageHeaderAccessor} + * while the parent class + * {@link org.springframework.messaging.simp.SimpMessageHeaderAccessor} manages + * common processing headers some of which are based on STOMP headers (e.g. + * destination, content-type, etc). + * + *

An instance of this class can also be created by wrapping an existing + * {@code Message}. That message may have been created with the more generic + * {@link org.springframework.messaging.simp.SimpMessageHeaderAccessor} in + * which case STOMP headers are created from common processing headers. + * In this case it is also necessary to invoke either + * {@link #updateStompCommandAsClientMessage()} or + * {@link #updateStompCommandAsServerMessage()} if sending a message and + * depending on whether a message is sent to a client or the message broker. * * @author Rossen Stoyanchev * @since 4.0 */ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { + private static final StompHeaderAccessorFactory factory = new DefaultStompHeaderAccessorFactory(); + private static final AtomicLong messageIdCounter = new AtomicLong(); + private static final long[] DEFAULT_HEARTBEAT = new long[] {0, 0}; + // STOMP header names public static final String STOMP_ID_HEADER = "id"; @@ -83,9 +101,6 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { public static final String STOMP_HEARTBEAT_HEADER = "heart-beat"; - private static final long[] DEFAULT_HEARTBEAT = new long[] {0, 0}; - - // Other header names private static final String COMMAND_HEADER = "stompCommand"; @@ -94,135 +109,113 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { /** - * A constructor for creating new STOMP message headers. + * A constructor for creating message headers from a parsed STOMP frame. */ - private StompHeaderAccessor(StompCommand command, Map> externalSourceHeaders) { - + StompHeaderAccessor(StompCommand command, Map> externalSourceHeaders) { super(command.getMessageType(), externalSourceHeaders); - - Assert.notNull(command, "Command must not be null"); setHeader(COMMAND_HEADER, command); - - if (externalSourceHeaders != null) { - setSimpMessageHeaders(command, externalSourceHeaders); - } - } - - private void setSimpMessageHeaders(StompCommand command, Map> extHeaders) { - - List values = extHeaders.get(StompHeaderAccessor.STOMP_DESTINATION_HEADER); - if (!CollectionUtils.isEmpty(values)) { - super.setDestination(values.get(0)); - } - - values = extHeaders.get(StompHeaderAccessor.STOMP_CONTENT_TYPE_HEADER); - if (!CollectionUtils.isEmpty(values)) { - super.setContentType(MimeTypeUtils.parseMimeType(values.get(0))); - } - - if (StompCommand.SUBSCRIBE.equals(command) || StompCommand.UNSUBSCRIBE.equals(command)) { - values = extHeaders.get(StompHeaderAccessor.STOMP_ID_HEADER); - if (!CollectionUtils.isEmpty(values)) { - super.setSubscriptionId(values.get(0)); - } - } - else if (StompCommand.MESSAGE.equals(command)) { - values = extHeaders.get(StompHeaderAccessor.STOMP_SUBSCRIPTION_HEADER); - if (!CollectionUtils.isEmpty(values)) { - super.setSubscriptionId(values.get(0)); - } - } - else if (StompCommand.CONNECT.equals(command)) { - if (!StringUtils.isEmpty(getPasscode())) { - setHeader(CREDENTIALS_HEADER, new StompPasscode(getPasscode())); - setPasscode("PROTECTED"); - } - } + updateSimpMessageHeadersFromStompHeaders(); } /** * A constructor for accessing and modifying existing message headers. + * Note that the message headers may not have been created from a STOMP frame + * but may have rather originated from using the more generic + * {@link org.springframework.messaging.simp.SimpMessageHeaderAccessor}. */ - private StompHeaderAccessor(Message message) { + StompHeaderAccessor(Message message) { super(message); + updateStompHeadersFromSimpMessageHeaders(); + } + + StompHeaderAccessor() { + super(SimpMessageType.HEARTBEAT, null); + } + + void updateSimpMessageHeadersFromStompHeaders() { + if (getNativeHeaders() == null) { + return; + } + String value = getFirstNativeHeader(STOMP_DESTINATION_HEADER); + if (value != null) { + super.setDestination(value); + } + value = getFirstNativeHeader(STOMP_CONTENT_TYPE_HEADER); + if (value != null) { + super.setContentType(MimeTypeUtils.parseMimeType(value)); + } + StompCommand command = getCommand(); + if (StompCommand.MESSAGE.equals(command)) { + value = getFirstNativeHeader(STOMP_SUBSCRIPTION_HEADER); + if (value != null) { + super.setSubscriptionId(value); + } + } + else if (StompCommand.SUBSCRIBE.equals(command) || StompCommand.UNSUBSCRIBE.equals(command)) { + value = getFirstNativeHeader(STOMP_ID_HEADER); + if (value != null) { + super.setSubscriptionId(value); + } + } + else if (StompCommand.CONNECT.equals(command)) { + protectPasscode(); + } + } + + private void updateStompHeadersFromSimpMessageHeaders() { + if (getDestination() != null) { + setNativeHeader(STOMP_DESTINATION_HEADER, getDestination()); + } + if (getContentType() != null) { + setNativeHeader(STOMP_CONTENT_TYPE_HEADER, getContentType().toString()); + } + trySetStompHeaderForSubscriptionId(); } /** - * Create {@link StompHeaderAccessor} for a new {@link Message}. + * Create an instance for the given STOMP command. */ public static StompHeaderAccessor create(StompCommand command) { - return new StompHeaderAccessor(command, null); + return factory.create(command); } /** - * Create {@link StompHeaderAccessor} from parsed STOP frame content. + * Create an instance for the given STOMP command and headers. */ public static StompHeaderAccessor create(StompCommand command, Map> headers) { - return new StompHeaderAccessor(command, headers); + return factory.create(command, headers); } /** - * Create {@link StompHeaderAccessor} from the headers of an existing {@link Message}. + * Create headers for a heartbeat. While a STOMP heartbeat frame does not + * have headers, a session id is needed for processing purposes at a minimum. + */ + public static StompHeaderAccessor createForHeartbeat() { + return factory.createForHeartbeat(); + } + + /** + * Create an instance from the payload and headers of the given Message. */ public static StompHeaderAccessor wrap(Message message) { - return new StompHeaderAccessor(message); + return factory.wrap(message); } - /** - * Return STOMP headers including original, wrapped STOMP headers (if any) plus - * additional header updates made through accessor methods. - */ @Override - public Map> toNativeHeaderMap() { - - Map> result = super.toNativeHeaderMap(); - - String destination = super.getDestination(); - if (destination != null) { - result.put(STOMP_DESTINATION_HEADER, Arrays.asList(destination)); - } - - MimeType contentType = super.getContentType(); - if (contentType != null) { - result.put(STOMP_CONTENT_TYPE_HEADER, Arrays.asList(contentType.toString())); - } - - if (getCommand() != null && getCommand().requiresSubscriptionId()) { - String subscriptionId = getSubscriptionId(); - if (subscriptionId != null) { - String name = StompCommand.MESSAGE.equals(getCommand()) ? STOMP_SUBSCRIPTION_HEADER : STOMP_ID_HEADER; - result.put(name, Arrays.asList(subscriptionId)); - } - else { - logger.warn(getCommand() + " frame does not have a subscription identifier" + this.toString()); - } - } - - if (StompCommand.MESSAGE.equals(getCommand()) && ((getMessageId() == null))) { - String messageId = getSessionId() + "-" + messageIdCounter.getAndIncrement(); - result.put(STOMP_MESSAGE_ID_HEADER, Arrays.asList(messageId)); - } - - return result; + protected MessageHeaderAccessor createAccessor(Message message) { + return factory.wrap(message); } - public Map> toStompHeaderMap() { - if (StompCommand.CONNECT.equals(getCommand())) { - StompPasscode credentials = (StompPasscode) getHeader(CREDENTIALS_HEADER); - if (credentials != null) { - Map> headers = toNativeHeaderMap(); - headers.put(STOMP_PASSCODE_HEADER, Arrays.asList(credentials.passcode)); - return headers; - } - } - return toNativeHeaderMap(); + Map> getNativeHeaders() { + @SuppressWarnings("unchecked") + Map> map = (Map>) getHeader(NATIVE_HEADERS); + return (map != null ? map : Collections.>emptyMap()); } public StompCommand updateStompCommandAsClientMessage() { - Assert.state(SimpMessageType.MESSAGE.equals(getMessageType()), - "Unexpected message type " + getMessage()); + Assert.state(SimpMessageType.MESSAGE.equals(getMessageType()), "Unexpected message type " + getMessage()); if (getCommand() == null) { setHeader(COMMAND_HEADER, StompCommand.SEND); @@ -236,26 +229,47 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { public void updateStompCommandAsServerMessage() { - Assert.state(SimpMessageType.MESSAGE.equals(getMessageType()), - "Unexpected message type " + getMessage()); + Assert.state(SimpMessageType.MESSAGE.equals(getMessageType()), "Unexpected message type " + getMessage()); - if ((getCommand() == null) || getCommand().equals(StompCommand.SEND)) { + StompCommand command = getCommand(); + if ((command == null) || StompCommand.SEND.equals(command)) { setHeader(COMMAND_HEADER, StompCommand.MESSAGE); } - else if (!getCommand().equals(StompCommand.MESSAGE)) { - throw new IllegalStateException("Unexpected STOMP command " + getCommand()); + else if (!StompCommand.MESSAGE.equals(command)) { + throw new IllegalStateException("Unexpected STOMP command " + command); + } + + trySetStompHeaderForSubscriptionId(); + + if (getMessageId() == null) { + String messageId = getSessionId() + "-" + messageIdCounter.getAndIncrement(); + setNativeHeader(STOMP_MESSAGE_ID_HEADER, messageId); } } + /** + * @return the STOMP command, or {@code null} if not yet set. + */ public StompCommand getCommand() { return (StompCommand) getHeader(COMMAND_HEADER); } + /** + * A static alternative for access to the STOMP command. + */ + public static StompCommand getCommand(Map headers) { + return (StompCommand) headers.get(COMMAND_HEADER); + } + public Set getAcceptVersion() { String rawValue = getFirstNativeHeader(STOMP_ACCEPT_VERSION_HEADER); return (rawValue != null) ? StringUtils.commaDelimitedListToSet(rawValue) : Collections.emptySet(); } + public boolean isHeartbeat() { + return (SimpMessageType.HEARTBEAT == getMessageType()); + } + public void setAcceptVersion(String acceptVersion) { setNativeHeader(STOMP_ACCEPT_VERSION_HEADER, acceptVersion); } @@ -288,9 +302,41 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { setNativeHeader(STOMP_CONTENT_TYPE_HEADER, contentType.toString()); } + @Override + public void setSubscriptionId(String subscriptionId) { + super.setSubscriptionId(subscriptionId); + trySetStompHeaderForSubscriptionId(); + } + + private void trySetStompHeaderForSubscriptionId() { + String subscriptionId = getSubscriptionId(); + if (subscriptionId != null) { + if (getCommand() != null && StompCommand.MESSAGE.equals(getCommand())) { + setNativeHeader(STOMP_SUBSCRIPTION_HEADER, subscriptionId); + } + else { + SimpMessageType messageType = getMessageType(); + if (SimpMessageType.SUBSCRIBE.equals(messageType) || SimpMessageType.UNSUBSCRIBE.equals(messageType)) { + setNativeHeader(STOMP_ID_HEADER, subscriptionId); + } + } + } + } + public Integer getContentLength() { - String contentLength = getFirstNativeHeader(STOMP_CONTENT_LENGTH_HEADER); - return StringUtils.hasText(contentLength) ? new Integer(contentLength) : null; + if (containsNativeHeader(STOMP_CONTENT_LENGTH_HEADER)) { + return Integer.valueOf(getFirstNativeHeader(STOMP_CONTENT_LENGTH_HEADER)); + } + return null; + } + + public static Integer getContentLength(Map> nativeHeaders) { + if (nativeHeaders.containsKey(STOMP_CONTENT_LENGTH_HEADER)) { + List values = nativeHeaders.get(STOMP_CONTENT_LENGTH_HEADER); + String value = (values != null ? values.get(0) : null); + return Integer.valueOf(value); + } + return null; } public void setContentLength(int contentLength) { @@ -298,7 +344,7 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { } public void setHeartbeat(long cx, long cy) { - setNativeHeader(STOMP_HEARTBEAT_HEADER, StringUtils.arrayToCommaDelimitedString(new Object[] {cx, cy})); + setNativeHeader(STOMP_HEARTBEAT_HEADER, StringUtils.arrayToCommaDelimitedString(new Object[]{cx, cy})); } public void setAck(String ack) { @@ -328,10 +374,31 @@ public class StompHeaderAccessor extends SimpMessageHeaderAccessor { public void setPasscode(String passcode) { setNativeHeader(STOMP_PASSCODE_HEADER, passcode); + protectPasscode(); } + private void protectPasscode() { + String value = getFirstNativeHeader(STOMP_PASSCODE_HEADER); + if (value != null && !"PROTECTED".equals(value)) { + setHeader(CREDENTIALS_HEADER, new StompPasscode(value)); + setNativeHeader(STOMP_PASSCODE_HEADER, "PROTECTED"); + } + } + + /** + * @return the passcode header value or {@code null}. + */ public String getPasscode() { - return getFirstNativeHeader(STOMP_PASSCODE_HEADER); + StompPasscode credentials = (StompPasscode) getHeader(CREDENTIALS_HEADER); + return (credentials != null ? credentials.passcode : null); + } + + /** + * A static alternative for access to the passcode header. + */ + public static String getPasscode(Map headers) { + StompPasscode credentials = (StompPasscode) headers.get(CREDENTIALS_HEADER); + return (credentials != null ? credentials.passcode : null); } public void setReceiptId(String receiptId) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorFactory.java new file mode 100644 index 0000000000..d1f3217fc0 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2014 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.messaging.Message; + +import java.util.List; +import java.util.Map; + +/** + * A factory for creating pre-configured instances of type + * {@link org.springframework.messaging.simp.stomp.StompHeaderAccessor}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public interface StompHeaderAccessorFactory { + + /** + * Create an instance for the given STOMP command. + */ + StompHeaderAccessor create(StompCommand command); + + /** + * Create an instance for the given STOMP command and headers. + */ + StompHeaderAccessor create(StompCommand command, Map> headers); + + /** + * Create headers for a heartbeat. While a STOMP heartbeat frame does not + * have headers, a session id is needed for processing purposes at a minimum. + */ + StompHeaderAccessor createForHeartbeat(); + + /** + * Create an instance from the payload and headers of the given Message. + */ + StompHeaderAccessor wrap(Message message); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java index 57ef7373df..a7a9dee4db 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java @@ -104,7 +104,7 @@ public abstract class AbstractMessageChannel implements MessageChannel, BeanName Assert.notNull(message, "Message must not be null"); if (logger.isTraceEnabled()) { - logger.trace("[" + this.beanName + "] sending message id=" + message.getHeaders().getId()); + logger.trace("[" + this.beanName + "] sending message=" + message); } message = this.interceptorChain.preSend(message, this); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptorChain.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptorChain.java index f28eee36cb..8d3a2a3100 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptorChain.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptorChain.java @@ -57,7 +57,7 @@ class ChannelInterceptorChain { public Message preSend(Message message, MessageChannel channel) { - UUID originalId = message.getHeaders().getId(); + Message originalMessage = message; for (ChannelInterceptor interceptor : this.interceptors) { message = interceptor.preSend(message, channel); if (message == null) { @@ -68,8 +68,8 @@ class ChannelInterceptorChain { } } if (logger.isDebugEnabled()) { - if (!message.getHeaders().getId().equals(originalId)) { - logger.debug("preSend returned modified message, new message id=" + message.getHeaders().getId()); + if (message != originalMessage) { + logger.debug("preSend returned modified message, new message=" + message); } } return message; @@ -77,7 +77,7 @@ class ChannelInterceptorChain { public void postSend(Message message, MessageChannel channel, boolean sent) { if (logger.isTraceEnabled()) { - logger.trace("postSend (sent=" + sent + ") message id " + message.getHeaders().getId()); + logger.trace("postSend (sent=" + sent + ")"); } for (ChannelInterceptor interceptor : this.interceptors) { interceptor.postSend(message, channel, sent); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java index 737543246a..59779eccac 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,6 +16,8 @@ package org.springframework.messaging.support; +import org.springframework.messaging.MessageHeaders; + import java.util.Map; /** @@ -43,12 +45,26 @@ public class ErrorMessage extends GenericMessage { /** * Create a new message with the given payload and headers. + * The content of the given header map is copied. * * @param payload the message payload, never {@code null} - * @param headers message headers + * @param headers message headers to use for initialization */ public ErrorMessage(Throwable payload, Map headers) { super(payload, headers); } + /** + * A constructor with the {@link MessageHeaders} instance to use. + * + *

Note: the given {@code MessageHeaders} instance is used + * directly in the new message, i.e. it is not copied. + * + * @param payload the message payload, never {@code null} + * @param headers message headers + */ + public ErrorMessage(Throwable payload, MessageHeaders headers) { + super(payload, headers); + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java b/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java index ae50a5ee9d..786f65e14c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java @@ -49,18 +49,33 @@ public class GenericMessage implements Message, Serializable { * @param payload the message payload, never {@code null} */ public GenericMessage(T payload) { - this(payload, null); + this(payload, new MessageHeaders(null)); } /** * Create a new message with the given payload and headers. + * The content of the given header map is copied. + * + * @param payload the message payload, never {@code null} + * @param headers message headers to use for initialization + */ + public GenericMessage(T payload, Map headers) { + this(payload, new MessageHeaders(headers)); + } + + /** + * A constructor with the {@link MessageHeaders} instance to use. + * + *

Note: the given {@code MessageHeaders} instance is used + * directly in the new message, i.e. it is not copied. * * @param payload the message payload, never {@code null} * @param headers message headers */ - public GenericMessage(T payload, Map headers) { + public GenericMessage(T payload, MessageHeaders headers) { + Assert.notNull(headers, "'headers' must not be null"); Assert.notNull(payload, "payload must not be null"); - this.headers = new MessageHeaders(headers); + this.headers = headers; this.payload = payload; } @@ -96,7 +111,7 @@ public class GenericMessage implements Message, Serializable { } if (obj != null && obj instanceof GenericMessage) { GenericMessage other = (GenericMessage) obj; - return (this.headers.getId().equals(other.headers.getId()) && + return (ObjectUtils.nullSafeEquals(this.headers.getId(), other.headers.getId()) && this.headers.equals(other.headers) && this.payload.equals(other.payload)); } return false; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java index d0cef013f5..deee2c24d5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java @@ -20,6 +20,7 @@ import java.util.Map; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; /** @@ -36,23 +37,27 @@ import org.springframework.util.Assert; */ public final class MessageBuilder { + private final Message originalMessage; + private final T payload; private MessageHeaderAccessor headerAccessor; - private final Message originalMessage; - - /** - * Private constructor to be invoked from the static factory methods only. - */ - private MessageBuilder(T payload, Message originalMessage) { - Assert.notNull(payload, "payload must not be null"); - this.payload = payload; - this.originalMessage = originalMessage; + private MessageBuilder(Message originalMessage) { + Assert.notNull(originalMessage, "'originalMessage' must not be null"); + this.payload = originalMessage.getPayload(); this.headerAccessor = new MessageHeaderAccessor(originalMessage); + this.originalMessage = originalMessage; } + private MessageBuilder(T payload, MessageHeaderAccessor accessor) { + Assert.notNull(payload, "'payload' must not be null"); + Assert.notNull(accessor, "'messageHeaderAccessor' must not be null"); + this.payload = payload; + this.headerAccessor = accessor; + this.originalMessage = null; + } /** * Create a builder for a new {@link Message} instance pre-populated with all of the @@ -62,26 +67,49 @@ public final class MessageBuilder { * @param message the Message from which the payload and all headers will be copied */ public static MessageBuilder fromMessage(Message message) { - Assert.notNull(message, "message must not be null"); - return new MessageBuilder(message.getPayload(), message); + return new MessageBuilder(message); } /** - * Create a builder for a new {@link Message} instance with the provided payload. - * @param payload the payload for the new message + * Create a new builder for a message with the given payload. + * @param payload the payload */ public static MessageBuilder withPayload(T payload) { - return new MessageBuilder(payload, null); + return new MessageBuilder(payload, new MessageHeaderAccessor()); } + /** + * A shortcut factory method for creating a message with the given payload + * and {@code MessageHeaders}. + * + *

Note: the given {@code MessageHeaders} instance is used + * directly in the new message, i.e. it is not copied. + * + * @param payload the payload to use, never {@code null} + * @param messageHeaders the headers to use, never {@code null} + * @return the created message + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static Message createMessage(T payload, MessageHeaders messageHeaders) { + Assert.notNull(payload, "'payload' must not be null"); + Assert.notNull(messageHeaders, "'messageHeaders' must not be null"); + if (payload instanceof Throwable) { + return (Message) new ErrorMessage((Throwable) payload, messageHeaders); + } + else { + return new GenericMessage(payload, messageHeaders); + } + } /** - * Set the message headers. - * @param headerAccessor the headers for the message + * Set the message headers to use by providing a {@code MessageHeaderAccessor}. + * + * @param accessor the headers to use */ - public MessageBuilder setHeaders(MessageHeaderAccessor headerAccessor) { - Assert.notNull(headerAccessor, "HeaderAccessor must not be null"); - this.headerAccessor = headerAccessor; + public MessageBuilder setHeaders(MessageHeaderAccessor accessor) { + Assert.notNull(accessor, "HeaderAccessor must not be null"); + this.headerAccessor = accessor; return this; } @@ -161,13 +189,17 @@ public final class MessageBuilder { @SuppressWarnings("unchecked") public Message build() { - if ((this.originalMessage != null) && !this.headerAccessor.isModified()) { + + if (this.originalMessage != null && !this.headerAccessor.isModified()) { return this.originalMessage; } + if (this.payload instanceof Throwable) { return (Message) new ErrorMessage((Throwable) this.payload, this.headerAccessor.toMap()); } - return new GenericMessage(this.payload, this.headerAccessor.toMap()); + else { + return new GenericMessage(this.payload, this.headerAccessor.toMap()); + } } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java index 0f65993049..c1036c6aba 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -29,19 +29,74 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; +import org.springframework.util.IdGenerator; import org.springframework.util.MimeType; 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. + * A base for classes providing strongly typed getters and setters as well as + * behavior around specific categories of headers (e.g. STOMP headers). + * Supports creating new headers (default constructor), modifying existing headers + * (when still mutable), or copying and modifying existing headers. * - *

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. + *

The {@link #getMessageHeaders()} method provides access to the underlying, + * fully-prepared {@code MessageHeaders} instance that can then be used as-is + * to create a single message as follows: + * + *

+ * MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor();
+ * headerAccessor.set("foo", "bar");
+ * Message message = MessageBuilder.createMessage("payload", headerAccessor.getMessageHeaders());
+ * 
+ * + *

After the above is executed, by default the {@code MessageHeaderAccessor} + * is immutable. It is also possible to leave it mutable, for example for further + * initialization of the message in the same thread: + * + *

+ * MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor();
+ * headerAccessor.set("foo", "bar");
+ * headerAccessor.setLeaveMutable(true);
+ * Message message = MessageBuilder.createMessage("payload", headerAccessor.getMessageHeaders());
+ *
+ * // later on in the same thread...
+ *
+ * MessageHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message);
+ * headerAccessor.set("bar", "baz");
+ * headerAccessor.setImmutable();
+ * 
+ * + *

To re-obtain the {@code MessageHeaderAccessor} for a {@code MessageHeaders} + * instance, use the following: + * + *

+ * MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor();
+ * headerAccessor.set("foo", "bar");
+ * Message message = MessageBuilder.createMessage("payload", headerAccessor.getMessageHeaders());
+ *
+ * // later on (any thread)...
+ * MessageHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message);
+ * headerAccessor.get("foo");
+ * 
+ * + *

To prepare multiple messages with the same {@code MessageHeaderAccessor} + * instance, use the code below. However note that this usage style does not allow + * re-obtaining a header accessor later on: + *

+ * MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor();
+ * MessageBuilder builder = MessageBuilder.withPayload("payload").setHeaders(headerAccessor);
+ *
+ * headerAccessor.setHeader("foo", "bar1");
+ * Message message1 = builder.build();
+ *
+ * headerAccessor.setHeader("foo", "bar2");
+ * Message message2 = builder.build();
+ *
+ * headerAccessor.setHeader("foo", "bar3");
+ * Message  message3 = builder.build();
+ * 
* * @author Rossen Stoyanchev * @since 4.0 @@ -50,62 +105,209 @@ public class MessageHeaderAccessor { protected Log logger = LogFactory.getLog(getClass()); + private final MutableMessageHeaders headers; - // wrapped read-only message headers - private final MessageHeaders originalHeaders; + private boolean modified; - // header updates - private final Map headers = new HashMap(4); + private boolean leaveMutable; + + private IdGenerator idGenerator; + + private boolean enableTimestamp = false; /** - * A constructor for creating new message headers. + * A constructor to create new headers. */ public MessageHeaderAccessor() { - this.originalHeaders = null; + this.headers = new MutableMessageHeaders(); } /** - * A constructor for accessing and modifying existing message headers. + * A constructor accepting the headers of an existing message to copy. */ public MessageHeaderAccessor(Message message) { - this.originalHeaders = (message != null) ? message.getHeaders() : null; + if (message != null) { + this.headers = new MutableMessageHeaders(message.getHeaders()); + MessageHeaderAccessor accessor = getAccessor(message, MessageHeaderAccessor.class); + if (accessor != null) { + this.idGenerator = accessor.idGenerator; + this.enableTimestamp = accessor.enableTimestamp; + } + } + else { + this.headers = new MutableMessageHeaders(); + } } + /** + * Return the original {@code MessageHeaderAccessor} used to create the headers + * of the given {@code Message}, or {@code null} if that's not available or if + * its type does not match the required type. + * + *

This is for cases where the existence of an accessor is strongly expected + * (to be followed up with an assertion) or will created if not provided. + * + * @return an accessor instance of the specified type or {@code null}. + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static T getAccessor(Message message, Class requiredType) { + return getAccessor(message.getHeaders(), requiredType); + } /** - * Return a header map including original, wrapped headers (if any) plus additional - * header updates made through accessor methods. + * A variation of {@link #getAccessor(org.springframework.messaging.Message, Class)} + * with a {@code MessageHeaders} instance instead of a {@code Message}. + * + *

This is for cases when a full message may not have been created yet. + * + * @return an accessor instance of the specified type or {@code null}. + * @since 4.1 + */ + @SuppressWarnings("unchecked") + public static T getAccessor(MessageHeaders messageHeaders, Class requiredType) { + if (messageHeaders instanceof MutableMessageHeaders) { + MutableMessageHeaders mutableHeaders = (MutableMessageHeaders) messageHeaders; + MessageHeaderAccessor headerAccessor = mutableHeaders.getMessageHeaderAccessor(); + if (requiredType.isAssignableFrom(headerAccessor.getClass())) { + return (T) headerAccessor; + } + } + return null; + } + + /** + * Return a mutable {@code MessageHeaderAccessor} for the given message attempting + * to match the type of accessor used to create the message headers, or otherwise + * wrapping the message with a {@code MessageHeaderAccessor} instance. + * + *

This is for cases where a header needs to be updated in generic code + * while preserving the accessor type for downstream processing. + * + * @return an accessor of the required type, never {@code null}. + * @since 4.1 + */ + public static MessageHeaderAccessor getMutableAccessor(Message message) { + if (message.getHeaders() instanceof MutableMessageHeaders) { + MutableMessageHeaders mutableHeaders = (MutableMessageHeaders) message.getHeaders(); + MessageHeaderAccessor accessor = mutableHeaders.getMessageHeaderAccessor(); + if (accessor != null) { + return (accessor.isMutable() ? accessor : accessor.createAccessor(message)); + } + } + return new MessageHeaderAccessor(message); + } + + protected MessageHeaderAccessor createAccessor(Message message) { + return new MessageHeaderAccessor(message); + } + + /** + * Return the underlying {@code MessageHeaders} instance. + * + *

Unless {@link #setLeaveMutable(boolean)} was set to {@code true}, after + * this call, the headers are immutable and this accessor can no longer + * modify them. + * + *

This method always returns the same {@code MessageHeaders} instance if + * invoked multiples times. To obtain a copy of the underlying headers instead + * use {@link #toMap()}. + */ + public MessageHeaders getMessageHeaders() { + this.headers.setIdAndTimestamp(); + if (!this.leaveMutable) { + setImmutable(); + } + return this.headers; + } + + /** + * Return a copy of the underlying header values. + * + *

This method can be invoked many times, with modifications in between + * where each new call returns a fresh copy of the current header values. */ public Map toMap() { - Map result = new HashMap(); - 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; + return new HashMap(this.headers); + } + + /** + * By default when {@link #getMessageHeaders()} is called, {@code "this"} + * {@code MessageHeaderAccessor} instance can no longer be used to modify the + * underlying message headers and the returned {@code MessageHeaders} is immutable. + * + *

However when this is set to {@code true}, the returned (underlying) + * {@code MessageHeaders} instance remains mutable. To make further modifications + * continue to use the same accessor instance or re-obtain it via:
+ * {@link org.springframework.messaging.support.MessageHeaderAccessor#getAccessor(org.springframework.messaging.Message, Class) + * MessageHeaderAccessor.getAccessor(Message, Class)} + * + *

When modifications are complete use {@link #setImmutable()} to prevent + * further changes. The intended use case for this mechanism is initialization + * of a Message within a single thread. + * + *

By default this is set to {@code false}. + * @since 4.1 + */ + public void setLeaveMutable(boolean leaveMutable) { + Assert.state(this.headers.isMutable(), "Already immutable"); + this.leaveMutable = leaveMutable; + } + + /** + * By default when {@link #getMessageHeaders()} is called, {@code "this"} + * {@code MessageHeaderAccessor} instance can no longer be used to modify the + * underlying message headers. However if {@link #setLeaveMutable(boolean)} + * is used, this method is necessary to indicate explicitly when the + * {@code MessageHeaders} instance should no longer be modified. + * @since 4.1 + */ + public void setImmutable() { + this.headers.setImmutable(); + } + + /** + * Whether the underlying headers can still be modified. + * @since 4.1 + */ + public boolean isMutable() { + return this.headers.isMutable(); + } + + /** + * A private mechanism for providing an alternate IdGenerator strategy. + * + *

By default this property is not set in which case the default IdGenerator + * of {@link org.springframework.messaging.MessageHeaders} is used. + * + * @see org.springframework.messaging.support.MessageHeaderAccessorFactorySupport + */ + void setIdGenerator(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + } + + /** + * A private mechanism to enable having a timestamp added to every message. + * + *

By default this property is set to false. + * + * @see org.springframework.messaging.support.MessageHeaderAccessorFactorySupport + */ + void setEnableTimestamp(boolean enableTimestamp) { + this.enableTimestamp = enableTimestamp; } public boolean isModified() { - return (!this.headers.isEmpty()); + return this.modified; + } + + protected void setModified(boolean modified) { + this.modified = modified; } 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; + return this.headers.get(headerName); } /** @@ -116,7 +318,22 @@ public class MessageHeaderAccessor { Assert.isTrue(!isReadOnly(name), "The '" + name + "' header is read-only."); verifyType(name, value); if (!ObjectUtils.nullSafeEquals(value, getHeader(name))) { - this.headers.put(name, value); + this.modified = true; + if (value != null) { + this.headers.getRawHeaders().put(name, value); + } + else { + this.headers.getRawHeaders().remove(name); + } + } + } + + 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."); + } } } @@ -144,7 +361,6 @@ public class MessageHeaderAccessor { if (StringUtils.hasLength(pattern)){ if (pattern.contains("*")){ headersToRemove.addAll(getMatchingHeaderNames(pattern, this.headers)); - headersToRemove.addAll(getMatchingHeaderNames(pattern, this.originalHeaders)); } else { headersToRemove.add(pattern); @@ -251,17 +467,59 @@ public class MessageHeaderAccessor { @Override public String toString() { - return getClass().getSimpleName() + " [originalHeaders=" + this.originalHeaders - + ", updated headers=" + this.headers + "]"; + return getClass().getSimpleName() + " [headers=" + this.headers + "]"; + } + + + @SuppressWarnings("serial") + private class MutableMessageHeaders extends MessageHeaders { + + private boolean immutable; + + + public MutableMessageHeaders() { + this(null); + } + + public MutableMessageHeaders(Map headers) { + super(headers, MessageHeaders.ID_VALUE_NONE, -1L); + } + + public MessageHeaderAccessor getMessageHeaderAccessor() { + return MessageHeaderAccessor.this; + } + + @Override + public Map getRawHeaders() { + Assert.state(!this.immutable, "Already immutable"); + return super.getRawHeaders(); + } + + public void setImmutable() { + this.immutable = true; + } + + public boolean isMutable() { + return !this.immutable; + } + + public void setIdAndTimestamp() { + if (getId() == null) { + IdGenerator idGenerator = (MessageHeaderAccessor.this.idGenerator != null) ? + MessageHeaderAccessor.this.idGenerator : + MessageHeaders.getIdGenerator(); + + UUID id = idGenerator.generateId(); + if (id != null && id != MessageHeaders.ID_VALUE_NONE) { + getRawHeaders().put(ID, id); + } + } + if (getTimestamp() == null) { + if (MessageHeaderAccessor.this.enableTimestamp) { + getRawHeaders().put(TIMESTAMP, System.currentTimeMillis()); + } + } + } } - 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."); - } - } - } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessorFactorySupport.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessorFactorySupport.java new file mode 100644 index 0000000000..7ed78bb208 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessorFactorySupport.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2014 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 org.springframework.util.IdGenerator; + +/** + * A support class for factories creating pre-configured instances of type + * {@link org.springframework.messaging.support.MessageHeaderAccessor}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class MessageHeaderAccessorFactorySupport { + + private IdGenerator idGenerator; + + private Boolean enableTimestamp; + + + protected MessageHeaderAccessorFactorySupport() { + } + + /** + * + * @param idGenerator + */ + public void setIdGenerator(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + } + + public IdGenerator getIdGenerator() { + return this.idGenerator; + } + + public void setEnableTimestamp(boolean enableTimestamp) { + this.enableTimestamp = enableTimestamp; + } + + public boolean isEnableTimestamp() { + return this.enableTimestamp; + } + + protected void updateMessageHeaderAccessor(MessageHeaderAccessor headerAccessor) { + if (this.idGenerator != null) { + headerAccessor.setIdGenerator(this.idGenerator); + } + if (this.enableTimestamp != null) { + headerAccessor.setEnableTimestamp(this.enableTimestamp); + } + } + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java index d83747c3d1..c7f7d623eb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,25 +16,26 @@ package org.springframework.messaging.support; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Map; import org.springframework.messaging.Message; +import org.springframework.util.Assert; 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 stores and provides read/write * access to message headers from an external source -- e.g. a Spring {@link Message} * created to represent a STOMP message received from a STOMP client or message broker. - * Native message headers are kept in a {@link MultiValueMap} under the key + * Native message headers are kept in a {@code Map>} under the key * {@link #NATIVE_HEADERS}. *

- * This class is not intended for direct use but is rather expected to be consumed - * through sub-classes such as + * This class is not intended for direct use but is rather expected to be used + * indirectly through protocol-specific sub-classes such as * {@link org.springframework.messaging.simp.stomp.StompHeaderAccessor StompHeaderAccessor}. * Such sub-classes may provide factory methods to translate message headers from * an external messaging source (e.g. STOMP) to Spring {@link Message} headers and @@ -49,107 +50,140 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { public static final String NATIVE_HEADERS = "nativeHeaders"; - // wrapped native headers - private final Map> originalNativeHeaders; - - // native header updates - private final MultiValueMap nativeHeaders = new LinkedMultiValueMap(4); - - /** - * A constructor for creating new headers, accepting an optional native header map. + * A protected constructor to create new headers. */ - protected NativeMessageHeaderAccessor(Map> nativeHeaders) { - this.originalNativeHeaders = nativeHeaders; + protected NativeMessageHeaderAccessor() { + this((Map>) null); } /** - * A constructor for accessing and modifying existing message headers. + * A protected constructor to create new headers. + * @param nativeHeaders native headers to create the message with, may be {@code null} + */ + protected NativeMessageHeaderAccessor(Map> nativeHeaders) { + if (!CollectionUtils.isEmpty(nativeHeaders)) { + setHeader(NATIVE_HEADERS, new LinkedMultiValueMap(nativeHeaders)); + } + } + + /** + * A protected constructor accepting the headers of an existing message to copy. */ protected NativeMessageHeaderAccessor(Message message) { super(message); - this.originalNativeHeaders = initNativeHeaders(message); - } - - private static Map> initNativeHeaders(Message message) { if (message != null) { @SuppressWarnings("unchecked") - Map> headers = (Map>) message.getHeaders().get(NATIVE_HEADERS); - if (headers != null) { - return headers; + Map> map = (Map>) getHeader(NATIVE_HEADERS); + if (map != null) { + // Force removal since setHeader checks for equality + removeHeader(NATIVE_HEADERS); + setHeader(NATIVE_HEADERS, new LinkedMultiValueMap(map)); } } - return null; } - @Override - public Map toMap() { - Map result = super.toMap(); - result.put(NATIVE_HEADERS, toNativeHeaderMap()); - return result; - } - - @Override - public boolean isModified() { - return (super.isModified() || (!this.nativeHeaders.isEmpty())); + @SuppressWarnings("unchecked") + private Map> getNativeHeaders() { + return (Map>) getHeader(NATIVE_HEADERS); } /** - * Return a map with native headers including original, wrapped headers (if any) plus - * additional header updates made through accessor methods. + * Return a copy of the native header values or an empty map. */ public Map> toNativeHeaderMap() { - Map> result = new HashMap>(); - if (this.originalNativeHeaders != null) { - result.putAll(this.originalNativeHeaders); - } - for (String key : this.nativeHeaders.keySet()) { - List value = this.nativeHeaders.get(key); - if (value == null) { - result.remove(key); - } - else { - result.put(key, value); + Map> map = getNativeHeaders(); + return (map != null ? new LinkedMultiValueMap(map) : Collections.>emptyMap()); + } + + @Override + public void setImmutable() { + if (isMutable()) { + Map> map = getNativeHeaders(); + if (map != null) { + // Force removal since setHeader checks for equality + removeHeader(NATIVE_HEADERS); + setHeader(NATIVE_HEADERS, Collections.>unmodifiableMap(map)); } + super.setImmutable(); } - return result; } /** - * Return all values for the specified native header or {@code null}. + * Whether the native header map contains the give header name. + */ + public boolean containsNativeHeader(String headerName) { + Map> map = getNativeHeaders(); + return (map != null ? map.containsKey(headerName) : false); + } + + /** + * @return all values for the specified native header or {@code null}. */ public List getNativeHeader(String headerName) { - if (this.nativeHeaders.containsKey(headerName)) { - return this.nativeHeaders.get(headerName); - } - else if (this.originalNativeHeaders != null) { - return this.originalNativeHeaders.get(headerName); + Map> map = getNativeHeaders(); + return (map != null ? map.get(headerName) : null); + } + + /** + * @return the first value for the specified native header of {@code null}. + */ + public String getFirstNativeHeader(String headerName) { + Map> map = getNativeHeaders(); + if (map != null) { + List values = map.get(headerName); + if (values != null) { + return values.get(0); + } } return null; } /** - * Return the first value for the specified native header of {@code null}. - */ - public String getFirstNativeHeader(String headerName) { - List values = getNativeHeader(headerName); - return CollectionUtils.isEmpty(values) ? null : values.get(0); - } - - /** - * Set the specified native header value. + * Set the specified native header value replacing existing values. */ public void setNativeHeader(String name, String value) { - if (!ObjectUtils.nullSafeEquals(value, getHeader(name))) { - this.nativeHeaders.set(name, value); + Assert.state(isMutable(), "Already immutable"); + Map> map = getNativeHeaders(); + if (value == null) { + if (map != null && map.get(name) != null) { + setModified(true); + map.remove(name); + } + return; + } + if (map == null) { + map = new LinkedMultiValueMap(4); + setHeader(NATIVE_HEADERS, map); + } + List values = new LinkedList(); + values.add(value); + if (!ObjectUtils.nullSafeEquals(values, getHeader(name))) { + setModified(true); + map.put(name, values); } } /** - * Add the specified native header value. + * Add the specified native header value to existing values. */ public void addNativeHeader(String name, String value) { - this.nativeHeaders.add(name, value); + Assert.state(isMutable(), "Already immutable"); + if (value == null) { + return; + } + Map> nativeHeaders = getNativeHeaders(); + if (nativeHeaders == null) { + nativeHeaders = new LinkedMultiValueMap(4); + setHeader(NATIVE_HEADERS, nativeHeaders); + } + List values = nativeHeaders.get(name); + if (values == null) { + values = new LinkedList(); + nativeHeaders.put(name, values); + } + values.add(value); + setModified(true); } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java b/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java index afc35ec452..0e557cce6a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/MessageHeadersTests.java @@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -53,6 +54,25 @@ public class MessageHeadersTests { assertNotSame(headers1.getTimestamp(), headers2.getTimestamp()); } + @Test + public void testTimestampProvided() throws Exception { + MessageHeaders headers = new MessageHeaders(null, null, 10L); + assertEquals(10L, (long) headers.getTimestamp()); + } + + @Test + public void testTimestampProvidedNullValue() throws Exception { + Map input = Collections.singletonMap(MessageHeaders.TIMESTAMP, 1L); + MessageHeaders headers = new MessageHeaders(input, null, null); + assertNotNull(headers.getTimestamp()); + } + + @Test + public void testTimestampNone() throws Exception { + MessageHeaders headers = new MessageHeaders(null, null, -1L); + assertNull(headers.getTimestamp()); + } + @Test public void testIdOverwritten() throws Exception { MessageHeaders headers1 = new MessageHeaders(null); @@ -66,6 +86,26 @@ public class MessageHeadersTests { assertNotNull(headers.getId()); } + @Test + public void testIdProvided() { + UUID id = new UUID(0L, 25L); + MessageHeaders headers = new MessageHeaders(null, id, null); + assertEquals(id, headers.getId()); + } + + @Test + public void testIdProvidedNullValue() { + Map input = Collections.singletonMap(MessageHeaders.ID, new UUID(0L, 25L)); + MessageHeaders headers = new MessageHeaders(input, null, null); + assertNotNull(headers.getId()); + } + + @Test + public void testIdNone() { + MessageHeaders headers = new MessageHeaders(null, MessageHeaders.ID_VALUE_NONE, null); + assertNull(headers.getId()); + } + @Test public void testNonTypedAccessOfHeaderValue() { Integer value = new Integer(123); @@ -148,7 +188,7 @@ public class MessageHeadersTests { class MyMH extends MessageHeaders { public MyMH() { - super(null, new UUID(0, id.incrementAndGet()), null); + super(null, new UUID(0, id.incrementAndGet()), -1L); } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java index ba0be7f848..8ce946d008 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolverTests.java @@ -17,6 +17,8 @@ package org.springframework.messaging.handler.annotation.support; import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -149,7 +151,7 @@ public class HeaderMethodArgumentResolverTests { public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor { protected TestMessageHeaderAccessor() { - super((Message) null); + super((Map>) null); } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java index 4d2846ec25..276f60a5a2 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/SimpMessagingTemplateTests.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.springframework.messaging.Message; import org.springframework.messaging.StubMessageChannel; import org.springframework.messaging.support.NativeMessageHeaderAccessor; +import org.springframework.util.LinkedMultiValueMap; import java.util.*; @@ -87,9 +88,9 @@ public class SimpMessagingTemplateTests { @Test public void convertAndSendWithCustomHeaderNonNative() { - Map headers = new HashMap(); + Map headers = new HashMap<>(); headers.put("key", "value"); - headers.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, Collections.emptyMap()); + headers.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, new LinkedMultiValueMap()); this.messagingTemplate.convertAndSend("/foo", "data", headers); List> messages = this.messageChannel.getMessages(); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/BufferingStompDecoderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/BufferingStompDecoderTests.java index ebdd9ce00c..35d7dd03c8 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/BufferingStompDecoderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/BufferingStompDecoderTests.java @@ -19,6 +19,7 @@ package org.springframework.messaging.simp.stomp; import org.junit.Test; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; +import org.springframework.util.LinkedMultiValueMap; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -175,6 +176,17 @@ public class BufferingStompDecoderTests { stompDecoder.decode(toByteBuffer(payload)); } + @Test + public void incompleteCommand() throws InterruptedException { + + BufferingStompDecoder stompDecoder = new BufferingStompDecoder(128); + String chunk = "MESSAG"; + + LinkedMultiValueMap headers = new LinkedMultiValueMap<>(); + List> messages = stompDecoder.decode(toByteBuffer(chunk), headers); + assertEquals(0, messages.size()); + } + private ByteBuffer toByteBuffer(String chunk) { return ByteBuffer.wrap(chunk.getBytes(Charset.forName("UTF-8"))); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompCodecTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompCodecTests.java index 0bf6f591a8..ac7b68f3f2 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompCodecTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompCodecTests.java @@ -25,6 +25,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.InvalidMimeTypeException; import reactor.function.Consumer; import reactor.function.Function; import reactor.io.Buffer; @@ -48,7 +49,7 @@ public class StompCodecTests { StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); assertEquals(StompCommand.DISCONNECT, headers.getCommand()); - assertEquals(0, headers.toStompHeaderMap().size()); + assertEquals(0, headers.toNativeHeaderMap().size()); assertEquals(0, frame.getPayload().length); } @@ -58,7 +59,7 @@ public class StompCodecTests { StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); assertEquals(StompCommand.DISCONNECT, headers.getCommand()); - assertEquals(0, headers.toStompHeaderMap().size()); + assertEquals(0, headers.toNativeHeaderMap().size()); assertEquals(0, frame.getPayload().length); } @@ -72,7 +73,7 @@ public class StompCodecTests { assertEquals(StompCommand.CONNECT, headers.getCommand()); - assertEquals(2, headers.toStompHeaderMap().size()); + assertEquals(2, headers.toNativeHeaderMap().size()); assertEquals("1.1", headers.getFirstNativeHeader("accept-version")); assertEquals("github.org", headers.getHost()); @@ -86,7 +87,7 @@ public class StompCodecTests { assertEquals(StompCommand.SEND, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(headers.toNativeHeaderMap().toString(), 1, headers.toNativeHeaderMap().size()); assertEquals("test", headers.getDestination()); String bodyText = new String(frame.getPayload()); @@ -95,15 +96,15 @@ public class StompCodecTests { @Test public void decodeFrameWithContentLength() { - Message frame = decode("SEND\ncontent-length:23\n\nThe body of the message\0"); - StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); + Message message = decode("SEND\ncontent-length:23\n\nThe body of the message\0"); + StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); assertEquals(StompCommand.SEND, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(1, headers.toNativeHeaderMap().size()); assertEquals(Integer.valueOf(23), headers.getContentLength()); - String bodyText = new String(frame.getPayload()); + String bodyText = new String(message.getPayload()); assertEquals("The body of the message", bodyText); } @@ -111,15 +112,15 @@ public class StompCodecTests { @Test public void decodeFrameWithInvalidContentLength() { - Message frame = decode("SEND\ncontent-length:-1\n\nThe body of the message\0"); - StompHeaderAccessor headers = StompHeaderAccessor.wrap(frame); + Message message = decode("SEND\ncontent-length:-1\n\nThe body of the message\0"); + StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); assertEquals(StompCommand.SEND, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(1, headers.toNativeHeaderMap().size()); assertEquals(Integer.valueOf(-1), headers.getContentLength()); - String bodyText = new String(frame.getPayload()); + String bodyText = new String(message.getPayload()); assertEquals("The body of the message", bodyText); } @@ -130,7 +131,7 @@ public class StompCodecTests { assertEquals(StompCommand.SEND, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(1, headers.toNativeHeaderMap().size()); assertEquals(Integer.valueOf(0), headers.getContentLength()); String bodyText = new String(frame.getPayload()); @@ -144,7 +145,7 @@ public class StompCodecTests { assertEquals(StompCommand.SEND, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(1, headers.toNativeHeaderMap().size()); assertEquals(Integer.valueOf(23), headers.getContentLength()); String bodyText = new String(frame.getPayload()); @@ -158,7 +159,7 @@ public class StompCodecTests { assertEquals(StompCommand.DISCONNECT, headers.getCommand()); - assertEquals(1, headers.toStompHeaderMap().size()); + assertEquals(1, headers.toNativeHeaderMap().size()); assertEquals("alpha:bravo\r\n\\", headers.getFirstNativeHeader("a:\r\n\\b")); } @@ -187,6 +188,11 @@ public class StompCodecTests { assertEquals(StompCommand.DISCONNECT, StompHeaderAccessor.wrap(messages.get(1)).getCommand()); } + @Test + public void decodeFrameWithIncompleteCommand() { + assertIncompleteDecode("MESSAG"); + } + @Test public void decodeFrameWithIncompleteHeader() { assertIncompleteDecode("SEND\ndestination"); @@ -206,6 +212,16 @@ public class StompCodecTests { assertIncompleteDecode("SEND\ncontent-length:23\n\nThe body of the mess"); } + @Test + public void decodeFrameWithIncompleteContentType() { + assertIncompleteDecode("SEND\ncontent-type:text/plain;charset=U"); + } + + @Test(expected = InvalidMimeTypeException.class) + public void decodeFrameWithInvalidContentType() { + assertIncompleteDecode("SEND\ncontent-type:text/plain;charset=U\n\nThe body\0"); + } + @Test(expected=StompConversionException.class) public void decodeFrameWithIncorrectTerminator() { decode("SEND\ncontent-length:23\n\nThe body of the message*"); @@ -233,7 +249,7 @@ public class StompCodecTests { public void encodeFrameWithNoHeadersAndNoBody() { StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); - Message frame = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); assertEquals("DISCONNECT\n\n\0", new StompCodec().encoder().apply(frame).asString()); } @@ -244,7 +260,7 @@ public class StompCodecTests { headers.setAcceptVersion("1.2"); headers.setHost("github.org"); - Message frame = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); String frameString = new StompCodec().encoder().apply(frame).asString(); @@ -257,9 +273,10 @@ public class StompCodecTests { StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT); headers.addNativeHeader("a:\r\n\\b", "alpha:bravo\r\n\\"); - Message frame = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build(); + Message frame = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()); - assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0", new StompCodec().encoder().apply(frame).asString()); + assertEquals("DISCONNECT\na\\c\\r\\n\\\\b:alpha\\cbravo\\r\\n\\\\\n\n\0", + new StompCodec().encoder().apply(frame).asString()); } @Test @@ -267,9 +284,10 @@ public class StompCodecTests { StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND); headers.addNativeHeader("a", "alpha"); - Message frame = MessageBuilder.withPayload("Message body".getBytes()).setHeaders(headers).build(); + Message frame = MessageBuilder.createMessage("Message body".getBytes(), headers.getMessageHeaders()); - assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0", new StompCodec().encoder().apply(frame).asString()); + assertEquals("SEND\na:alpha\ncontent-length:12\n\nMessage body\0", + new StompCodec().encoder().apply(frame).asString()); } private void assertIncompleteDecode(String partialFrame) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorTests.java index 93fa9e8624..5df376dec6 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompHeaderAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,22 +16,28 @@ package org.springframework.messaging.simp.stomp; +import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; +import org.hamcrest.CoreMatchers; import org.junit.Test; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.util.AlternativeJdkIdGenerator; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeTypeUtils; import org.springframework.util.MultiValueMap; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** - * Test fixture for {@link StompHeaderAccessor}. + * Unit tests for {@link StompHeaderAccessor}. * * @author Rossen Stoyanchev * @since 4.0 @@ -99,17 +105,18 @@ public class StompHeaderAccessorTests { extHeaders.add(StompHeaderAccessor.STOMP_LOGIN_HEADER, "joe"); extHeaders.add(StompHeaderAccessor.STOMP_PASSCODE_HEADER, "joe123"); - StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT, extHeaders); + StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.CONNECT, extHeaders); - assertEquals(StompCommand.CONNECT, headers.getCommand()); - assertEquals(SimpMessageType.CONNECT, headers.getMessageType()); - assertNotNull(headers.getHeader("stompCredentials")); - assertEquals("joe", headers.getLogin()); - assertEquals("PROTECTED", headers.getPasscode()); + assertEquals(StompCommand.CONNECT, headerAccessor.getCommand()); + assertEquals(SimpMessageType.CONNECT, headerAccessor.getMessageType()); + assertNotNull(headerAccessor.getHeader("stompCredentials")); + assertEquals("joe", headerAccessor.getLogin()); + assertEquals("joe123", headerAccessor.getPasscode()); + assertThat(headerAccessor.toString(), CoreMatchers.containsString("passcode=[PROTECTED]")); - Map> output = headers.toStompHeaderMap(); + Map> output = headerAccessor.toNativeHeaderMap(); assertEquals("joe", output.get(StompHeaderAccessor.STOMP_LOGIN_HEADER).get(0)); - assertEquals("joe123", output.get(StompHeaderAccessor.STOMP_PASSCODE_HEADER).get(0)); + assertEquals("PROTECTED", output.get(StompHeaderAccessor.STOMP_PASSCODE_HEADER).get(0)); } @Test @@ -145,10 +152,11 @@ public class StompHeaderAccessorTests { headers.setSubscriptionId("s1"); headers.setDestination("/d"); headers.setContentType(MimeTypeUtils.APPLICATION_JSON); + headers.updateStompCommandAsServerMessage(); Map> actual = headers.toNativeHeaderMap(); - assertEquals(4, actual.size()); + assertEquals(actual.toString(), 4, actual.size()); assertEquals("s1", actual.get(StompHeaderAccessor.STOMP_SUBSCRIPTION_HEADER).get(0)); assertEquals("/d", actual.get(StompHeaderAccessor.STOMP_DESTINATION_HEADER).get(0)); assertEquals("application/json", actual.get(StompHeaderAccessor.STOMP_CONTENT_TYPE_HEADER).get(0)); @@ -158,15 +166,30 @@ public class StompHeaderAccessorTests { @Test public void toNativeHeadersContentType() { - Message message = MessageBuilder.withPayload(new byte[0]) - .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_ATOM_XML).build(); + SimpMessageHeaderAccessor simpHeaderAccessor = SimpMessageHeaderAccessor.create(); + simpHeaderAccessor.setContentType(MimeTypeUtils.APPLICATION_ATOM_XML); + Message message = MessageBuilder.createMessage(new byte[0], simpHeaderAccessor.getMessageHeaders()); - StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); - Map> map = headers.toNativeHeaderMap(); + StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(message); + Map> map = stompHeaderAccessor.toNativeHeaderMap(); assertEquals("application/atom+xml", map.get(StompHeaderAccessor.STOMP_CONTENT_TYPE_HEADER).get(0)); } + @Test + public void encodeConnectWithLoginAndPasscode() throws UnsupportedEncodingException { + + MultiValueMap extHeaders = new LinkedMultiValueMap<>(); + extHeaders.add(StompHeaderAccessor.STOMP_LOGIN_HEADER, "joe"); + extHeaders.add(StompHeaderAccessor.STOMP_PASSCODE_HEADER, "joe123"); + + StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.CONNECT, extHeaders); + Message message = MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()); + byte[] bytes = new StompEncoder().encode(message); + + assertEquals("CONNECT\nlogin:joe\npasscode:joe123\n\n\0", new String(bytes, "UTF-8")); + } + @Test public void modifyCustomNativeHeader() { @@ -187,5 +210,34 @@ public class StompHeaderAccessorTests { assertNotNull("abc123", actual.get("accountId").get(0)); } + @Test + public void messageIdAndTimestampDefaultBehavior() { + StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.SEND); + MessageHeaders headers = headerAccessor.getMessageHeaders(); + + assertNull(headers.getId()); + assertNull(headers.getTimestamp()); + } + + @Test + public void messageIdAndTimestampEnabled() { + DefaultStompHeaderAccessorFactory factory = new DefaultStompHeaderAccessorFactory(); + factory.setIdGenerator(new AlternativeJdkIdGenerator()); + factory.setEnableTimestamp(true); + + StompHeaderAccessor headerAccessor = factory.create(StompCommand.SEND); + MessageHeaders headers = headerAccessor.getMessageHeaders(); + + assertNotNull(headers.getId()); + assertNotNull(headers.getTimestamp()); + } + + @Test + public void getAccessor() { + StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.CONNECT); + Message message = MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()); + + assertSame(headerAccessor, MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class)); + } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java index d62aa56289..2cef1a3669 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -21,17 +21,25 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.util.IdGenerator; import static org.junit.Assert.*; /** * @author Mark Fisher + * @author Rossen Stoyanchev */ public class MessageBuilderTests { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test public void testSimpleMessageCreation() { Message message = MessageBuilder.withPayload("foo").build(); @@ -42,7 +50,7 @@ public class MessageBuilderTests { public void testHeaderValues() { Message message = MessageBuilder.withPayload("test") .setHeader("foo", "bar") - .setHeader("count", new Integer(123)) + .setHeader("count", 123) .build(); assertEquals("bar", message.getHeaders().get("foo", String.class)); assertEquals(new Integer(123), message.getHeaders().get("count", Integer.class)); @@ -153,11 +161,11 @@ public class MessageBuilderTests { @Test public void testCopySameHeaderValuesNotModifiedSameMessage() throws Exception { Date current = new Date(); - Map originalHeaders = new HashMap(); + Map originalHeaders = new HashMap<>(); originalHeaders.put("b", "xyz"); originalHeaders.put("c", current); Message original = MessageBuilder.withPayload("foo").setHeader("a", 123).copyHeaders(originalHeaders).build(); - Map newHeaders = new HashMap(); + Map newHeaders = new HashMap<>(); newHeaders.put("a", 123); newHeaders.put("b", "xyz"); newHeaders.put("c", current); @@ -165,4 +173,61 @@ public class MessageBuilderTests { assertEquals(original, result); } + @Test + public void testBuildMessageWithMutableHeaders() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setLeaveMutable(true); + MessageHeaders headers = accessor.getMessageHeaders(); + Message message = MessageBuilder.createMessage("payload", headers); + accessor.setHeader("foo", "bar"); + + assertEquals("bar", headers.get("foo")); + assertSame(accessor, MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class)); + } + + @Test + public void testBuildMessageWithDefaultMutability() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + MessageHeaders headers = accessor.getMessageHeaders(); + Message message = MessageBuilder.createMessage("foo", headers); + + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Already immutable"); + accessor.setHeader("foo", "bar"); + + assertSame(accessor, MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class)); + } + + @Test + public void testBuildMessageWithoutIdAndTimestamp() { + MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor(); + headerAccessor.setIdGenerator(new IdGenerator() { + @Override + public UUID generateId() { + return MessageHeaders.ID_VALUE_NONE; + } + }); + Message message = MessageBuilder.createMessage("foo", headerAccessor.getMessageHeaders()); + assertNull(message.getHeaders().getId()); + assertNull(message.getHeaders().getTimestamp()); + } + + @Test + public void testBuildMultipleMessages() { + MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor(); + MessageBuilder messageBuilder = MessageBuilder.withPayload("payload").setHeaders(headerAccessor); + + headerAccessor.setHeader("foo", "bar1"); + Message message1 = messageBuilder.build(); + + headerAccessor.setHeader("foo", "bar2"); + Message message2 = messageBuilder.build(); + + headerAccessor.setHeader("foo", "bar3"); + Message message3 = messageBuilder.build(); + + assertEquals("bar1", message1.getHeaders().get("foo")); + assertEquals("bar2", message2.getHeaders().get("foo")); + assertEquals("bar3", message3.getHeaders().get("foo")); + } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageHeaderAccessorTests.java index c1200ea60a..52e01ee850 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/MessageHeaderAccessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/MessageHeaderAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,14 +16,21 @@ package org.springframework.messaging.support; -import java.util.Collections; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.UUID; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.util.IdGenerator; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * Test fixture for {@link MessageHeaderAccessor}. @@ -32,55 +39,229 @@ import static org.junit.Assert.*; */ public class MessageHeaderAccessorTests { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Test - public void empty() { - MessageHeaderAccessor headers = new MessageHeaderAccessor(); - assertEquals(Collections.emptyMap(), headers.toMap()); + public void newEmptyHeaders() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + assertEquals(0, accessor.toMap().size()); } @Test - public void wrapMessage() { - Map original = new HashMap<>(); - original.put("foo", "bar"); - original.put("bar", "baz"); - GenericMessage message = new GenericMessage<>("payload", original); + public void existingHeaders() throws InterruptedException { + Map map = new HashMap<>(); + map.put("foo", "bar"); + map.put("bar", "baz"); + GenericMessage message = new GenericMessage<>("payload", map); - MessageHeaderAccessor headers = new MessageHeaderAccessor(message); - Map actual = headers.toMap(); + MessageHeaderAccessor accessor = new MessageHeaderAccessor(message); + MessageHeaders actual = accessor.getMessageHeaders(); - assertEquals(4, actual.size()); - assertNotNull(actual.get(MessageHeaders.ID)); - assertNotNull(actual.get(MessageHeaders.TIMESTAMP)); + assertEquals(3, actual.size()); assertEquals("bar", actual.get("foo")); assertEquals("baz", actual.get("bar")); } @Test - public void wrapMessageAndModifyHeaders() { - Map original = new HashMap<>(); - original.put("foo", "bar"); - original.put("bar", "baz"); - GenericMessage message = new GenericMessage<>("payload", original); + public void existingHeadersModification() throws InterruptedException { + Map map = new HashMap<>(); + map.put("foo", "bar"); + map.put("bar", "baz"); + GenericMessage message = new GenericMessage<>("payload", map); - MessageHeaderAccessor headers = new MessageHeaderAccessor(message); - headers.setHeader("foo", "BAR"); - Map actual = headers.toMap(); + Thread.sleep(50); - assertEquals(4, actual.size()); - assertNotNull(actual.get(MessageHeaders.ID)); - assertNotNull(actual.get(MessageHeaders.TIMESTAMP)); + MessageHeaderAccessor accessor = new MessageHeaderAccessor(message); + accessor.setHeader("foo", "BAR"); + MessageHeaders actual = accessor.getMessageHeaders(); + + assertEquals(3, actual.size()); + assertNotEquals(message.getHeaders().getId(), actual.getId()); assertEquals("BAR", actual.get("foo")); assertEquals("baz", actual.get("bar")); } @Test - public void copyHeadersNullMap() { + public void copyHeadersFromNullMap() { MessageHeaderAccessor headers = new MessageHeaderAccessor(); headers.copyHeaders(null); headers.copyHeadersIfAbsent(null); - assertEquals(0, headers.toMap().size()); + assertEquals(1, headers.getMessageHeaders().size()); + assertEquals(new HashSet<>(Arrays.asList("id")), headers.getMessageHeaders().keySet()); + } + + @Test + public void toMap() { + + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + + accessor.setHeader("foo", "bar1"); + Map map1 = accessor.toMap(); + + accessor.setHeader("foo", "bar2"); + Map map2 = accessor.toMap(); + + accessor.setHeader("foo", "bar3"); + Map map3 = accessor.toMap(); + + assertEquals(1, map1.size()); + assertEquals(1, map2.size()); + assertEquals(1, map3.size()); + + assertEquals("bar1", map1.get("foo")); + assertEquals("bar2", map2.get("foo")); + assertEquals("bar3", map3.get("foo")); + } + + @Test + public void leaveMutable() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setHeader("foo", "bar"); + accessor.setLeaveMutable(true); + MessageHeaders headers = accessor.getMessageHeaders(); + Message message = MessageBuilder.createMessage("payload", headers); + + accessor.setHeader("foo", "baz"); + + assertEquals("baz", headers.get("foo")); + assertSame(accessor, MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class)); + } + + @Test + public void leaveMutableDefaultBehavior() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setHeader("foo", "bar"); + MessageHeaders headers = accessor.getMessageHeaders(); + Message message = MessageBuilder.createMessage("payload", headers); + + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Already immutable"); + accessor.setLeaveMutable(true); + + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Already immutable"); + accessor.setHeader("foo", "baz"); + + assertEquals("bar", headers.get("foo")); + assertSame(accessor, MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class)); + } + + @Test + public void getAccessor() { + MessageHeaderAccessor expected = new MessageHeaderAccessor(); + Message message = MessageBuilder.createMessage("payload", expected.getMessageHeaders()); + assertSame(expected, MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class)); + } + + @Test + public void getMutableAccessorSameInstance() { + TestMessageHeaderAccessor expected = new TestMessageHeaderAccessor(); + expected.setLeaveMutable(true); + Message message = MessageBuilder.createMessage("payload", expected.getMessageHeaders()); + + MessageHeaderAccessor actual = MessageHeaderAccessor.getMutableAccessor(message); + assertNotNull(actual); + assertTrue(actual.isMutable()); + assertSame(expected, actual); + } + + @Test + public void getMutableAccessorNewInstance() { + Message message = MessageBuilder.withPayload("payload").build(); + + MessageHeaderAccessor actual = MessageHeaderAccessor.getMutableAccessor(message); + assertNotNull(actual); + assertTrue(actual.isMutable()); + } + + @Test + public void getMutableAccessorNewInstanceMatchingType() { + TestMessageHeaderAccessor expected = new TestMessageHeaderAccessor(); + Message message = MessageBuilder.createMessage("payload", expected.getMessageHeaders()); + + MessageHeaderAccessor actual = MessageHeaderAccessor.getMutableAccessor(message); + assertNotNull(actual); + assertTrue(actual.isMutable()); + assertEquals(TestMessageHeaderAccessor.class, actual.getClass()); + } + + @Test + public void timestampEnabled() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setEnableTimestamp(true); + assertNotNull(accessor.getMessageHeaders().getTimestamp()); + } + + @Test + public void timestampDefaultBehavior() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + assertNull(accessor.getMessageHeaders().getTimestamp()); + } + + @Test + public void timestampBehaviorCopyFromExistingMessage() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setEnableTimestamp(true); + Message message = MessageBuilder.createMessage("payload", accessor.getMessageHeaders()); + MessageHeaderAccessor secondAccessor = new MessageHeaderAccessor(message); + assertNotNull(secondAccessor.getMessageHeaders().getTimestamp()); + } + + @Test + public void idGeneratorCustom() { + final UUID id = new UUID(0L, 23L); + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setIdGenerator(new IdGenerator() { + @Override + public UUID generateId() { + return id; + } + }); + assertSame(id, accessor.getMessageHeaders().getId()); + } + + @Test + public void idGeneratorDefaultBehavior() { + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + assertNotNull(accessor.getMessageHeaders().getId()); + } + + @Test + public void idGeneratorCopyFromExistingMessage() { + final UUID id = new UUID(0L, 23L); + MessageHeaderAccessor accessor = new MessageHeaderAccessor(); + accessor.setIdGenerator(new IdGenerator() { + @Override + public UUID generateId() { + return id; + } + }); + Message message = MessageBuilder.createMessage("payload", accessor.getMessageHeaders()); + MessageHeaderAccessor secondAccessor = new MessageHeaderAccessor(message); + assertSame(id, secondAccessor.getMessageHeaders().getId()); + } + + + public static class TestMessageHeaderAccessor extends MessageHeaderAccessor { + + private TestMessageHeaderAccessor() { + } + + private TestMessageHeaderAccessor(Message message) { + super(message); + } + + public static TestMessageHeaderAccessor wrap(Message message) { + return new TestMessageHeaderAccessor(message); + } + + @Override + protected TestMessageHeaderAccessor createAccessor(Message message) { + return wrap(message); + } } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java index d07469cf5e..0edd824762 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -19,16 +19,19 @@ package org.springframework.messaging.support; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; /** * Test fixture for {@link NativeMessageHeaderAccessor}. @@ -37,81 +40,78 @@ import static org.junit.Assert.*; */ public class NativeMessageHeaderAccessorTests { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Test - public void originalNativeHeaders() { - MultiValueMap original = new LinkedMultiValueMap<>(); - original.add("foo", "bar"); - original.add("bar", "baz"); + public void createFromNativeHeaderMap() { + MultiValueMap inputNativeHeaders = new LinkedMultiValueMap<>(); + inputNativeHeaders.add("foo", "bar"); + inputNativeHeaders.add("bar", "baz"); - NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(original); - Map actual = headers.toMap(); + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(inputNativeHeaders); + Map actual = headerAccessor.toMap(); - assertEquals(1, actual.size()); + assertEquals(actual.toString(), 1, actual.size()); assertNotNull(actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); - assertEquals(original, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + assertEquals(inputNativeHeaders, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + assertNotSame(inputNativeHeaders, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); } @Test - public void wrapMessage() { + public void createFromMessage() { + MultiValueMap inputNativeHeaders = new LinkedMultiValueMap<>(); + inputNativeHeaders.add("foo", "bar"); + inputNativeHeaders.add("bar", "baz"); - MultiValueMap originalNativeHeaders = new LinkedMultiValueMap<>(); - originalNativeHeaders.add("foo", "bar"); - originalNativeHeaders.add("bar", "baz"); + Map inputHeaders = new HashMap(); + inputHeaders.put("a", "b"); + inputHeaders.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, inputNativeHeaders); - Map original = new HashMap(); - original.put("a", "b"); - original.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, originalNativeHeaders); + GenericMessage message = new GenericMessage<>("p", inputHeaders); + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(message); + Map actual = headerAccessor.toMap(); - GenericMessage message = new GenericMessage<>("p", original); - - NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(message); - Map actual = headers.toMap(); - - assertEquals(4, actual.size()); - assertNotNull(actual.get(MessageHeaders.ID)); - assertNotNull(actual.get(MessageHeaders.TIMESTAMP)); + assertEquals(2, actual.size()); assertEquals("b", actual.get("a")); assertNotNull(actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); - assertEquals(originalNativeHeaders, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + assertEquals(inputNativeHeaders, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + assertNotSame(inputNativeHeaders, actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); } @Test - public void wrapNullMessage() { - NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor((Message) null); - Map actual = headers.toMap(); + public void createFromMessageNull() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor((Message) null); - assertEquals(1, actual.size()); + Map actual = headerAccessor.toMap(); + assertEquals(0, actual.size()); @SuppressWarnings("unchecked") - Map> actualNativeHeaders = - (Map>) actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS); - + Map> actualNativeHeaders = headerAccessor.toNativeHeaderMap(); assertEquals(Collections.emptyMap(), actualNativeHeaders); } @Test - public void wrapMessageAndModifyHeaders() { + public void createFromMessageAndModify() { - MultiValueMap originalNativeHeaders = new LinkedMultiValueMap<>(); - originalNativeHeaders.add("foo", "bar"); - originalNativeHeaders.add("bar", "baz"); + MultiValueMap inputNativeHeaders = new LinkedMultiValueMap<>(); + inputNativeHeaders.add("foo", "bar"); + inputNativeHeaders.add("bar", "baz"); - Map original = new HashMap(); - original.put("a", "b"); - original.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, originalNativeHeaders); + Map nativeHeaders = new HashMap(); + nativeHeaders.put("a", "b"); + nativeHeaders.put(NativeMessageHeaderAccessor.NATIVE_HEADERS, inputNativeHeaders); - GenericMessage message = new GenericMessage<>("p", original); + GenericMessage message = new GenericMessage<>("p", nativeHeaders); - NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(message); - headers.setHeader("a", "B"); - headers.setNativeHeader("foo", "BAR"); + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(message); + headerAccessor.setHeader("a", "B"); + headerAccessor.setNativeHeader("foo", "BAR"); - Map actual = headers.toMap(); + Map actual = headerAccessor.toMap(); - assertEquals(4, actual.size()); - assertNotNull(actual.get(MessageHeaders.ID)); - assertNotNull(actual.get(MessageHeaders.TIMESTAMP)); + assertEquals(2, actual.size()); assertEquals("B", actual.get("a")); @SuppressWarnings("unchecked") @@ -123,4 +123,112 @@ public class NativeMessageHeaderAccessorTests { assertEquals(Arrays.asList("baz"), actualNativeHeaders.get("bar")); } -} + @Test + public void setNativeHeader() { + MultiValueMap nativeHeaders = new LinkedMultiValueMap<>(); + nativeHeaders.add("foo", "bar"); + + NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); + headers.setNativeHeader("foo", "baz"); + + assertEquals(Arrays.asList("baz"), headers.getNativeHeader("foo")); + } + + @Test + public void setNativeHeaderNullValue() { + MultiValueMap nativeHeaders = new LinkedMultiValueMap<>(); + nativeHeaders.add("foo", "bar"); + + NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); + headers.setNativeHeader("foo", null); + + assertNull(headers.getNativeHeader("foo")); + } + + @Test + public void setNativeHeaderLazyInit() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.setNativeHeader("foo", "baz"); + + assertEquals(Arrays.asList("baz"), headerAccessor.getNativeHeader("foo")); + } + + @Test + public void setNativeHeaderLazyInitNullValue() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.setNativeHeader("foo", null); + + assertNull(headerAccessor.getNativeHeader("foo")); + assertNull(headerAccessor.getMessageHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + } + + @Test + public void setNativeHeaderImmutable() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.setNativeHeader("foo", "bar"); + headerAccessor.setImmutable(); + + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Already immutable"); + headerAccessor.setNativeHeader("foo", "baz"); + } + + @Test + public void addNativeHeader() { + MultiValueMap nativeHeaders = new LinkedMultiValueMap<>(); + nativeHeaders.add("foo", "bar"); + + NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); + headers.addNativeHeader("foo", "baz"); + + assertEquals(Arrays.asList("bar", "baz"), headers.getNativeHeader("foo")); + } + + @Test + public void addNativeHeaderNullValue() { + MultiValueMap nativeHeaders = new LinkedMultiValueMap<>(); + nativeHeaders.add("foo", "bar"); + + NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); + headers.addNativeHeader("foo", null); + + assertEquals(Arrays.asList("bar"), headers.getNativeHeader("foo")); + } + + @Test + public void addNativeHeaderLazyInit() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.addNativeHeader("foo", "bar"); + + assertEquals(Arrays.asList("bar"), headerAccessor.getNativeHeader("foo")); + } + + @Test + public void addNativeHeaderLazyInitNullValue() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.addNativeHeader("foo", null); + + assertNull(headerAccessor.getNativeHeader("foo")); + assertNull(headerAccessor.getMessageHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS)); + } + + @Test + public void addNativeHeaderImmutable() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.addNativeHeader("foo", "bar"); + headerAccessor.setImmutable(); + + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Already immutable"); + headerAccessor.addNativeHeader("foo", "baz"); + } + + @Test + public void setImmutableIdempotent() { + NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); + headerAccessor.addNativeHeader("foo", "bar"); + headerAccessor.setImmutable(); + headerAccessor.setImmutable(); + } + +} \ No newline at end of file diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java index 35fc8d268a..788c6a6a3c 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java @@ -268,7 +268,7 @@ public class StompSubProtocolHandlerTests { assertNotNull(headers.getSessionAttributes()); assertEquals("joe", headers.getUser().getName()); assertEquals("guest", headers.getLogin()); - assertEquals("PROTECTED", headers.getPasscode()); + assertEquals("guest", headers.getPasscode()); assertArrayEquals(new long[] {10000, 10000}, headers.getHeartbeat()); assertEquals(new HashSet<>(Arrays.asList("1.1","1.0")), headers.getAcceptVersion());