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 323b5ad1aa..ef7130f54a 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-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -39,76 +39,62 @@ import org.springframework.util.PatternMatchUtils; import org.springframework.util.StringUtils; /** - * 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, modifying existing headers (when still mutable), - * or copying and modifying existing headers. - * - *

The method {@link #getMessageHeaders()} provides access to the underlying, - * fully-prepared {@link MessageHeaders} that can then be used as-is (i.e. - * without copying) to create a single message as follows: + * Wrapper around {@link MessageHeaders} that provides extra features such as + * strongly typed accessors for specific headers, the ability to leave headers + * in a {@link Message} mutable, and the option to suppress automatic generation + * of {@link MessageHeaders#ID id} and {@link MessageHeaders#TIMESTAMP + * timesteamp} headers. Sub-classes such as {@link NativeMessageHeaderAccessor} + * and others provide support for managing processing vs external source headers + * as well as protocol specific headers. * + *

Below is a workflow to initialize headers via {@code MessageHeaderAccessor}, + * or one of its sub-classes, then create a {@link Message}, and then re-obtain + * the accessor possibly from a different component: *

+ * // Create a message with headers
  * MessageHeaderAccessor accessor = new MessageHeaderAccessor();
  * accessor.setHeader("foo", "bar");
- * Message message = MessageBuilder.createMessage("payload", accessor.getMessageHeaders());
+ * MessageHeaders headers = accessor.getMessageHeaders();
+ * Message message = MessageBuilder.createMessage("payload", headers);
+ *
+ * // Later on
+ * MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message);
+ * Assert.notNull(accessor, "No MessageHeaderAccessor");
  * 
* - *

After the above, by default the {@code MessageHeaderAccessor} becomes - * immutable. However it is possible to leave it mutable for further initialization - * in the same thread, for example: - * + *

In order for the above to work, all participating components must use + * {@code MessageHeaders} to create, access, or modify headers, or otherwise + * {@link MessageHeaderAccessor#getAccessor(Message, Class)} will return null. + * Below is a workflow that shows how headers are created and left mutable, + * then modified possibly by a different component, and finally made immutable + * perhaps before the possibility of being accessed on a different thread: *

+ * // Create a message with mutable headers
  * MessageHeaderAccessor accessor = new MessageHeaderAccessor();
  * accessor.setHeader("foo", "bar");
  * accessor.setLeaveMutable(true);
- * Message message = MessageBuilder.createMessage("payload", accessor.getMessageHeaders());
- *
- * // later on in the same thread...
+ * MessageHeaders headers = accessor.getMessageHeaders();
+ * Message message = MessageBuilder.createMessage("payload", headers);
  *
+ * // Later on
+ * MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message);
+ * if (accessor.isMutable()) {
+ *     // It's mutable, just change the headers
+ *     accessor.setHeader("bar", "baz");
+ * }
+ * else {
+ *     // It's not, so get a mutable copy, change and re-create
+ *     accessor = MessageHeaderAccessor.getMutableAccessor(message);
+ *     accessor.setHeader("bar", "baz");
+ *     accessor.setLeaveMutable(true); // leave mutable again or not?
+ *     message = MessageBuilder.createMessage(message.getPayload(), accessor);
+ * }
+ *
+ * // Make the accessor immutable
  * MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message);
- * accessor.setHeader("bar", "baz");
  * accessor.setImmutable();
  * 
* - *

The method {@link #toMap()} returns a copy of the underlying headers. It can - * be used to prepare multiple messages from the same {@code MessageHeaderAccessor} - * instance: - *

- * MessageHeaderAccessor accessor = new MessageHeaderAccessor();
- * MessageBuilder builder = MessageBuilder.withPayload("payload").setHeaders(accessor);
- *
- * accessor.setHeader("foo", "bar1");
- * Message message1 = builder.build();
- *
- * accessor.setHeader("foo", "bar2");
- * Message message2 = builder.build();
- *
- * accessor.setHeader("foo", "bar3");
- * Message  message3 = builder.build();
- * 
- * - *

However note that with the above style, the header accessor is shared and - * cannot be re-obtained later on. Alternatively it is also possible to create - * one {@code MessageHeaderAccessor} per message: - * - *

- * MessageHeaderAccessor accessor1 = new MessageHeaderAccessor();
- * accessor.set("foo", "bar1");
- * Message message1 = MessageBuilder.createMessage("payload", accessor1.getMessageHeaders());
- *
- * MessageHeaderAccessor accessor2 = new MessageHeaderAccessor();
- * accessor.set("foo", "bar2");
- * Message message2 = MessageBuilder.createMessage("payload", accessor2.getMessageHeaders());
- *
- * MessageHeaderAccessor accessor3 = new MessageHeaderAccessor();
- * accessor.set("foo", "bar3");
- * Message message3 = MessageBuilder.createMessage("payload", accessor3.getMessageHeaders());
- * 
- * - *

Note that the above examples aim to demonstrate the general idea of using - * header accessors. The most likely usage however is through subclasses. - * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 4.0 @@ -568,6 +554,21 @@ public class MessageHeaderAccessor { // Static factory methods + /** + * 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 + * (followed up with an assertion) or where an accessor will be created otherwise. + * @param message the message to get an accessor for + * @return an accessor instance of the specified type, or {@code null} if none + * @since 5.1.19 + */ + @Nullable + public static MessageHeaderAccessor getAccessor(Message message) { + return getAccessor(message.getHeaders(), null); + } + /** * 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 @@ -627,6 +628,11 @@ public class MessageHeaderAccessor { } + /** + * Extension of {@link MessageHeaders} that helps to preserve the link to + * the outer {@link MessageHeaderAccessor} instance that created it as well + * as keeps track of whether headers are still mutable. + */ @SuppressWarnings("serial") private class MutableMessageHeaders extends MessageHeaders { 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 624f0964d8..b9ecf08b5a 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-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -30,40 +30,34 @@ 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 {@code Map>} under the key - * {@link #NATIVE_HEADERS}. + * {@link MessageHeaderAccessor} sub-class that supports storage and access of + * headers from an external source such as a message broker. Headers from the + * external source are kept separate from other headers, in a sub-map under the + * key {@link #NATIVE_HEADERS}. This allows separating processing headers from + * headers that need to be sent to or received from the external source. * - *

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 - * reversely to translate Spring {@link Message} headers to a message to send to an - * external source. + *

This class is likely to be used through indirectly through a protocol + * specific sub-class that also provide factory methods to translate + * message headers to an from an external messaging source. * * @author Rossen Stoyanchev * @since 4.0 */ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { - /** - * The header name used to store native headers. - */ + /** The header name used to store native headers. */ public static final String NATIVE_HEADERS = "nativeHeaders"; /** - * A protected constructor to create new headers. + * Protected constructor to create a new instance. */ protected NativeMessageHeaderAccessor() { this((Map>) null); } /** - * A protected constructor to create new headers. + * Protected constructor to create an instance with the given native headers. * @param nativeHeaders native headers to create the message with (may be {@code null}) */ protected NativeMessageHeaderAccessor(@Nullable Map> nativeHeaders) { @@ -73,7 +67,7 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { } /** - * A protected constructor accepting the headers of an existing message to copy. + * Protected constructor that copies headers from another message. */ protected NativeMessageHeaderAccessor(@Nullable Message message) { super(message); @@ -88,6 +82,10 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { } } + + /** + * Sub-classes can use this method to access the "native" headers sub-map. + */ @SuppressWarnings("unchecked") @Nullable protected Map> getNativeHeaders() { @@ -95,7 +93,7 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { } /** - * Return a copy of the native header values or an empty map. + * Return a copy of the native headers sub-map, or an empty map. */ public Map> toNativeHeaderMap() { Map> map = getNativeHeaders(); @@ -124,8 +122,7 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { } /** - * Return all values for the specified native header. - * or {@code null} if none. + * Return the values for the specified native header, if present. */ @Nullable public List getNativeHeader(String headerName) { @@ -134,8 +131,7 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { } /** - * Return the first value for the specified native header, - * or {@code null} if none. + * Return the first value for the specified native header, if present. */ @Nullable public String getFirstNativeHeader(String headerName) { @@ -151,6 +147,8 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { /** * Set the specified native header value replacing existing values. + *

In order for this to work, the accessor must be {@link #isMutable() + * mutable}. See {@link MessageHeaderAccessor} for details. */ public void setNativeHeader(String name, @Nullable String value) { Assert.state(isMutable(), "Already immutable"); @@ -176,6 +174,8 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { /** * Add the specified native header value to existing values. + *

In order for this to work, the accessor must be {@link #isMutable() + * mutable}. See {@link MessageHeaderAccessor} for details. */ public void addNativeHeader(String name, @Nullable String value) { Assert.state(isMutable(), "Already immutable"); @@ -199,6 +199,11 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor { headers.forEach((key, values) -> values.forEach(value -> addNativeHeader(key, value))); } + /** + * Remove the specified native header value replacing existing values. + *

In order for this to work, the accessor must be {@link #isMutable() + * mutable}. See {@link MessageHeaderAccessor} for details. + */ @Nullable public List removeNativeHeader(String name) { Assert.state(isMutable(), "Already immutable"); 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 3902a06b47..5f87e5699a 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-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -112,8 +112,8 @@ public class NativeMessageHeaderAccessorTests { (Map>) actual.get(NativeMessageHeaderAccessor.NATIVE_HEADERS); assertThat(actualNativeHeaders).isNotNull(); - assertThat(actualNativeHeaders.get("foo")).isEqualTo(Arrays.asList("BAR")); - assertThat(actualNativeHeaders.get("bar")).isEqualTo(Arrays.asList("baz")); + assertThat(actualNativeHeaders.get("foo")).isEqualTo(Collections.singletonList("BAR")); + assertThat(actualNativeHeaders.get("bar")).isEqualTo(Collections.singletonList("baz")); } @Test @@ -124,7 +124,7 @@ public class NativeMessageHeaderAccessorTests { NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); headers.setNativeHeader("foo", "baz"); - assertThat(headers.getNativeHeader("foo")).isEqualTo(Arrays.asList("baz")); + assertThat(headers.getNativeHeader("foo")).isEqualTo(Collections.singletonList("baz")); } @Test @@ -143,7 +143,7 @@ public class NativeMessageHeaderAccessorTests { NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); headerAccessor.setNativeHeader("foo", "baz"); - assertThat(headerAccessor.getNativeHeader("foo")).isEqualTo(Arrays.asList("baz")); + assertThat(headerAccessor.getNativeHeader("foo")).isEqualTo(Collections.singletonList("baz")); } @Test @@ -161,9 +161,9 @@ public class NativeMessageHeaderAccessorTests { headerAccessor.setNativeHeader("foo", "bar"); headerAccessor.setImmutable(); - assertThatIllegalStateException().isThrownBy(() -> - headerAccessor.setNativeHeader("foo", "baz")) - .withMessageContaining("Already immutable"); + assertThatIllegalStateException() + .isThrownBy(() -> headerAccessor.setNativeHeader("foo", "baz")) + .withMessageContaining("Already immutable"); } @Test @@ -185,7 +185,7 @@ public class NativeMessageHeaderAccessorTests { NativeMessageHeaderAccessor headers = new NativeMessageHeaderAccessor(nativeHeaders); headers.addNativeHeader("foo", null); - assertThat(headers.getNativeHeader("foo")).isEqualTo(Arrays.asList("bar")); + assertThat(headers.getNativeHeader("foo")).isEqualTo(Collections.singletonList("bar")); } @Test @@ -193,7 +193,7 @@ public class NativeMessageHeaderAccessorTests { NativeMessageHeaderAccessor headerAccessor = new NativeMessageHeaderAccessor(); headerAccessor.addNativeHeader("foo", "bar"); - assertThat(headerAccessor.getNativeHeader("foo")).isEqualTo(Arrays.asList("bar")); + assertThat(headerAccessor.getNativeHeader("foo")).isEqualTo(Collections.singletonList("bar")); } @Test @@ -211,9 +211,9 @@ public class NativeMessageHeaderAccessorTests { headerAccessor.addNativeHeader("foo", "bar"); headerAccessor.setImmutable(); - assertThatIllegalStateException().isThrownBy(() -> - headerAccessor.addNativeHeader("foo", "baz")) - .withMessageContaining("Already immutable"); + assertThatIllegalStateException() + .isThrownBy(() -> headerAccessor.addNativeHeader("foo", "baz")) + .withMessageContaining("Already immutable"); } @Test