diff --git a/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java index 13ab0ad8dc2..e7d2716f84d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java @@ -108,7 +108,7 @@ public IntegrationMessageHeaderAccessor(@Nullable Message message) { * @see #isReadOnly(String) */ public void setReadOnlyHeaders(String... readOnlyHeaders) { - Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not be contain null items."); + Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not contain null items."); if (!ObjectUtils.isEmpty(readOnlyHeaders)) { this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders)); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/BaseMessageBuilder.java b/spring-integration-core/src/main/java/org/springframework/integration/support/BaseMessageBuilder.java new file mode 100644 index 00000000000..a63408985fe --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/BaseMessageBuilder.java @@ -0,0 +1,335 @@ +/* + * Copyright 2024 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 + * + * https://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.integration.support; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * The {@link AbstractIntegrationMessageBuilder} extension for the default logic to build message. + * The {@link MessageBuilder} is fully based on this class. + * This abstract class can be used for creating custom {@link Message} instances. + * For that purpose its {@link #build()} method has to be overridden. + * The custom {@link Message} type could be used, for example, to hide sensitive information + * from payload and headers when message is logged. + * For this goal there would be enough to override {@link GenericMessage#toString()} + * and filter out (or mask) those headers which container such sensitive information. + * + * @param the payload type. + * @param the target builder class type. + * + * @author Artem Bilan + * + * @since 6.4 + * + * @see MessageBuilder + * @see MessageBuilderFactory + */ +public abstract class BaseMessageBuilder> + extends AbstractIntegrationMessageBuilder { + + private static final Log LOGGER = LogFactory.getLog(BaseMessageBuilder.class); + + private final T payload; + + private final IntegrationMessageHeaderAccessor headerAccessor; + + @Nullable + private final Message originalMessage; + + private volatile boolean modified; + + private String[] readOnlyHeaders; + + protected BaseMessageBuilder(T payload, @Nullable Message originalMessage) { + Assert.notNull(payload, "payload must not be null"); + this.payload = payload; + this.originalMessage = originalMessage; + this.headerAccessor = new IntegrationMessageHeaderAccessor(originalMessage); + if (originalMessage != null) { + this.modified = (!this.payload.equals(originalMessage.getPayload())); + } + } + + @Override + public T getPayload() { + return this.payload; + } + + @Override + public Map getHeaders() { + return this.headerAccessor.toMap(); + } + + @Nullable + @Override + public V getHeader(String key, Class type) { + return this.headerAccessor.getHeader(key, type); + } + + /** + * Set the value for the given header name. If the provided value is {@code null}, the header will be removed. + * @param headerName The header name. + * @param headerValue The header value. + * @return this MessageBuilder. + */ + @Override + public B setHeader(String headerName, @Nullable Object headerValue) { + this.headerAccessor.setHeader(headerName, headerValue); + return _this(); + } + + /** + * Set the value for the given header name only if the header name is not already associated with a value. + * @param headerName The header name. + * @param headerValue The header value. + * @return this MessageBuilder. + */ + @Override + public B setHeaderIfAbsent(String headerName, Object headerValue) { + this.headerAccessor.setHeaderIfAbsent(headerName, headerValue); + return _this(); + } + + /** + * Removes all headers provided via array of 'headerPatterns'. As the name suggests the array + * may contain simple matching patterns for header names. Supported pattern styles are: + * {@code xxx*}, {@code *xxx}, {@code *xxx*} and {@code xxx*yyy}. + * @param headerPatterns The header patterns. + * @return this MessageBuilder. + */ + @Override + public B removeHeaders(String... headerPatterns) { + this.headerAccessor.removeHeaders(headerPatterns); + return _this(); + } + + /** + * Remove the value for the given header name. + * @param headerName The header name. + * @return this MessageBuilder. + */ + @Override + public B removeHeader(String headerName) { + if (!this.headerAccessor.isReadOnly(headerName)) { + this.headerAccessor.removeHeader(headerName); + } + else if (LOGGER.isInfoEnabled()) { + LOGGER.info("The header [" + headerName + "] is ignored for removal because it is is readOnly."); + } + return _this(); + } + + /** + * Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use + * {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values + * will never be overwritten. + * @param headersToCopy The headers to copy. + * @return this MessageBuilder. + * @see MessageHeaders#ID + * @see MessageHeaders#TIMESTAMP + */ + @Override + public B copyHeaders(@Nullable Map headersToCopy) { + this.headerAccessor.copyHeaders(headersToCopy); + return _this(); + } + + /** + * Copy the name-value pairs from the provided Map. This operation will not override any existing values. + * @param headersToCopy The headers to copy. + * @return this MessageBuilder. + */ + @Override + public B copyHeadersIfAbsent(@Nullable Map headersToCopy) { + if (headersToCopy != null) { + for (Map.Entry entry : headersToCopy.entrySet()) { + String headerName = entry.getKey(); + if (!this.headerAccessor.isReadOnly(headerName)) { + this.headerAccessor.setHeaderIfAbsent(headerName, entry.getValue()); + } + } + } + return _this(); + } + + @SuppressWarnings("unchecked") + @Override + @Nullable + protected List> getSequenceDetails() { + return (List>) this.headerAccessor.getHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS); + } + + @Override + @Nullable + protected Object getCorrelationId() { + return this.headerAccessor.getCorrelationId(); + } + + @Override + protected Object getSequenceNumber() { + return this.headerAccessor.getSequenceNumber(); + } + + @Override + protected Object getSequenceSize() { + return this.headerAccessor.getSequenceSize(); + } + + @Override + public B pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) { + super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize); + return _this(); + } + + @Override + public B popSequenceDetails() { + super.popSequenceDetails(); + return _this(); + } + + @Override + public B setExpirationDate(@Nullable Long expirationDate) { + super.setExpirationDate(expirationDate); + return _this(); + } + + @Override + public B setExpirationDate(@Nullable Date expirationDate) { + super.setExpirationDate(expirationDate); + return _this(); + } + + @Override + public B setCorrelationId(Object correlationId) { + super.setCorrelationId(correlationId); + return _this(); + } + + @Override + public B setReplyChannel(MessageChannel replyChannel) { + super.setReplyChannel(replyChannel); + return _this(); + } + + @Override + public B setReplyChannelName(String replyChannelName) { + super.setReplyChannelName(replyChannelName); + return _this(); + } + + @Override + public B setErrorChannel(MessageChannel errorChannel) { + super.setErrorChannel(errorChannel); + return _this(); + } + + @Override + public B setErrorChannelName(String errorChannelName) { + super.setErrorChannelName(errorChannelName); + return _this(); + } + + @Override + public B setSequenceNumber(Integer sequenceNumber) { + super.setSequenceNumber(sequenceNumber); + return _this(); + } + + @Override + public B setSequenceSize(Integer sequenceSize) { + super.setSequenceSize(sequenceSize); + return _this(); + } + + @Override + public B setPriority(Integer priority) { + super.setPriority(priority); + return _this(); + } + + /** + * Specify a list of headers which should be considered as read only + * and prohibited from being populated in the message. + * @param readOnlyHeaders the list of headers for {@code readOnly} mode. + * Defaults to {@link MessageHeaders#ID} and {@link MessageHeaders#TIMESTAMP}. + * @return the current {@link BaseMessageBuilder} + * @see IntegrationMessageHeaderAccessor#isReadOnly(String) + */ + public B readOnlyHeaders(@Nullable String... readOnlyHeaders) { + this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null; + if (readOnlyHeaders != null) { + this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders); + } + return _this(); + } + + /** + * Return an original message instance if it is not modified and does not have read-only headers. + * If payload is an instance of {@link Throwable}, then an {@link ErrorMessage} is built. + * Otherwise, a new instance of {@link GenericMessage} is produced. + * This method can be overridden to provide any custom message implementations. + * @return the message instance + * @see #getPayload() + * @see #getHeaders() + */ + @Override + @SuppressWarnings("unchecked") + public Message build() { + if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null + && !containsReadOnly(this.originalMessage.getHeaders())) { + + return this.originalMessage; + } + if (payload instanceof Throwable throwable) { + return (Message) new ErrorMessage(throwable, getHeaders()); + } + return new GenericMessage<>(payload, getHeaders()); + } + + private boolean containsReadOnly(MessageHeaders headers) { + if (!ObjectUtils.isEmpty(this.readOnlyHeaders)) { + for (String readOnly : this.readOnlyHeaders) { + if (headers.containsKey(readOnly)) { + return true; + } + } + } + return false; + } + + @SuppressWarnings("unchecked") + private B _this() { + return (B) this; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java b/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java index 8e812c1f4a8..bc3c7c59818 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/MessageBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,23 +16,10 @@ package org.springframework.integration.support; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * The default message builder; creates immutable {@link GenericMessage}s. @@ -48,52 +35,17 @@ * @author Gary Russell * @author Artem Bilan */ -public final class MessageBuilder extends AbstractIntegrationMessageBuilder { - - private static final Log LOGGER = LogFactory.getLog(MessageBuilder.class); - - private final T payload; - - private final IntegrationMessageHeaderAccessor headerAccessor; - - @Nullable - private final Message originalMessage; - - private volatile boolean modified; - - private String[] readOnlyHeaders; +public final class MessageBuilder extends BaseMessageBuilder> { /** * Private constructor to be invoked from the static factory methods only. */ private MessageBuilder(T payload, @Nullable Message originalMessage) { - Assert.notNull(payload, "payload must not be null"); - this.payload = payload; - this.originalMessage = originalMessage; - this.headerAccessor = new IntegrationMessageHeaderAccessor(originalMessage); - if (originalMessage != null) { - this.modified = (!this.payload.equals(originalMessage.getPayload())); - } - } - - @Override - public T getPayload() { - return this.payload; - } - - @Override - public Map getHeaders() { - return this.headerAccessor.toMap(); - } - - @Nullable - @Override - public V getHeader(String key, Class type) { - return this.headerAccessor.getHeader(key, type); + super(payload, originalMessage); } /** - * Create a builder for a new {@link Message} instance pre-populated with all of the headers copied from the + * Create a builder for a new {@link Message} instance pre-populated with all the headers copied from the * provided message. The payload of the provided Message will also be used as the payload for the new message. * @param message the Message from which the payload and all headers will be copied * @param The type of the payload. @@ -114,230 +66,4 @@ public static MessageBuilder withPayload(T payload) { return new MessageBuilder<>(payload, null); } - /** - * Set the value for the given header name. If the provided value is null, the header will be removed. - * @param headerName The header name. - * @param headerValue The header value. - * @return this MessageBuilder. - */ - @Override - public MessageBuilder setHeader(String headerName, @Nullable Object headerValue) { - this.headerAccessor.setHeader(headerName, headerValue); - return this; - } - - /** - * Set the value for the given header name only if the header name is not already associated with a value. - * @param headerName The header name. - * @param headerValue The header value. - * @return this MessageBuilder. - */ - @Override - public MessageBuilder setHeaderIfAbsent(String headerName, Object headerValue) { - this.headerAccessor.setHeaderIfAbsent(headerName, headerValue); - return this; - } - - /** - * Removes all headers provided via array of 'headerPatterns'. As the name suggests the array - * may contain simple matching patterns for header names. Supported pattern styles are: - * "xxx*", "*xxx", "*xxx*" and "xxx*yyy". - * @param headerPatterns The header patterns. - * @return this MessageBuilder. - */ - @Override - public MessageBuilder removeHeaders(String... headerPatterns) { - this.headerAccessor.removeHeaders(headerPatterns); - return this; - } - - /** - * Remove the value for the given header name. - * @param headerName The header name. - * @return this MessageBuilder. - */ - @Override - public MessageBuilder removeHeader(String headerName) { - if (!this.headerAccessor.isReadOnly(headerName)) { - this.headerAccessor.removeHeader(headerName); - } - else if (LOGGER.isInfoEnabled()) { - LOGGER.info("The header [" + headerName + "] is ignored for removal because it is is readOnly."); - } - return this; - } - - /** - * Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use { - * {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values - * will never be overwritten. - * @param headersToCopy The headers to copy. - * @return this MessageBuilder. - * @see MessageHeaders#ID - * @see MessageHeaders#TIMESTAMP - */ - @Override - public MessageBuilder copyHeaders(@Nullable Map headersToCopy) { - this.headerAccessor.copyHeaders(headersToCopy); - return this; - } - - /** - * Copy the name-value pairs from the provided Map. This operation will not overwrite any existing values. - * @param headersToCopy The headers to copy. - * @return this MessageBuilder. - */ - @Override - public MessageBuilder copyHeadersIfAbsent(@Nullable Map headersToCopy) { - if (headersToCopy != null) { - for (Map.Entry entry : headersToCopy.entrySet()) { - String headerName = entry.getKey(); - if (!this.headerAccessor.isReadOnly(headerName)) { - this.headerAccessor.setHeaderIfAbsent(headerName, entry.getValue()); - } - } - } - return this; - } - - @SuppressWarnings("unchecked") - @Override - @Nullable - protected List> getSequenceDetails() { - return (List>) this.headerAccessor.getHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS); - } - - @Override - @Nullable - protected Object getCorrelationId() { - return this.headerAccessor.getCorrelationId(); - } - - @Override - protected Object getSequenceNumber() { - return this.headerAccessor.getSequenceNumber(); - } - - @Override - protected Object getSequenceSize() { - return this.headerAccessor.getSequenceSize(); - } - - /* - * The following overrides (delegating to super) are provided to ease the - * pain for existing applications that use the builder API and expect - * a MessageBuilder to be returned. - */ - @Override - public MessageBuilder pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) { - super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize); - return this; - } - - @Override - public MessageBuilder popSequenceDetails() { - super.popSequenceDetails(); - return this; - } - - @Override - public MessageBuilder setExpirationDate(@Nullable Long expirationDate) { - super.setExpirationDate(expirationDate); - return this; - } - - @Override - public MessageBuilder setExpirationDate(@Nullable Date expirationDate) { - super.setExpirationDate(expirationDate); - return this; - } - - @Override - public MessageBuilder setCorrelationId(Object correlationId) { - super.setCorrelationId(correlationId); - return this; - } - - @Override - public MessageBuilder setReplyChannel(MessageChannel replyChannel) { - super.setReplyChannel(replyChannel); - return this; - } - - @Override - public MessageBuilder setReplyChannelName(String replyChannelName) { - super.setReplyChannelName(replyChannelName); - return this; - } - - @Override - public MessageBuilder setErrorChannel(MessageChannel errorChannel) { - super.setErrorChannel(errorChannel); - return this; - } - - @Override - public MessageBuilder setErrorChannelName(String errorChannelName) { - super.setErrorChannelName(errorChannelName); - return this; - } - - @Override - public MessageBuilder setSequenceNumber(Integer sequenceNumber) { - super.setSequenceNumber(sequenceNumber); - return this; - } - - @Override - public MessageBuilder setSequenceSize(Integer sequenceSize) { - super.setSequenceSize(sequenceSize); - return this; - } - - @Override - public MessageBuilder setPriority(Integer priority) { - super.setPriority(priority); - return this; - } - - /** - * Specify a list of headers which should be considered as read only - * and prohibited from being populated in the message. - * @param readOnlyHeaders the list of headers for {@code readOnly} mode. - * Defaults to {@link MessageHeaders#ID} and {@link MessageHeaders#TIMESTAMP}. - * @return the current {@link MessageBuilder} - * @since 4.3.2 - * @see IntegrationMessageHeaderAccessor#isReadOnly(String) - */ - public MessageBuilder readOnlyHeaders(String... readOnlyHeaders) { - this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null; - this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders); - return this; - } - - @Override - @SuppressWarnings("unchecked") - public Message build() { - if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null - && !containsReadOnly(this.originalMessage.getHeaders())) { - - 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()); - } - - private boolean containsReadOnly(MessageHeaders headers) { - if (!ObjectUtils.isEmpty(this.readOnlyHeaders)) { - for (String readOnly : this.readOnlyHeaders) { - if (headers.containsKey(readOnly)) { - return true; - } - } - } - return false; - } - } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/MessageBuilderTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/MessageBuilderTests.java index dcec01ce770..d5ab8a6199f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/MessageBuilderTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/MessageBuilderTests.java @@ -16,16 +16,22 @@ package org.springframework.integration.support; -import org.junit.Test; +import java.io.Serial; +import java.util.Map; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Artem Bilan * @since 4.3.10 - * */ public class MessageBuilderTests { @@ -45,4 +51,71 @@ public void testReadOnlyHeaders() { assertThat(message.getHeaders().get("qux")).isNull(); } + @Test + public void personalInfoHeadersAreMaskedWithCustomMessage() { + Message message = + MessageBuilder.withPayload("some_user") + .setHeader("password", "some_password") + .build(); + + Message piiMessage = new PiiMessageBuilderFactory().fromMessage(message).build(); + + assertThat(piiMessage).isInstanceOf(PiiMessage.class); + assertThat(piiMessage.getPayload()).isEqualTo("some_user"); + assertThat(piiMessage.getHeaders().get("password")).isEqualTo("some_password"); + assertThat(piiMessage.toString()) + .doesNotContain("some_password") + .contains("******"); + } + + private static class PiiMessageBuilderFactory implements MessageBuilderFactory { + + @Override + public PiiMessageBuilder fromMessage(Message message) { + return new PiiMessageBuilder<>(message.getPayload(), message); + } + + @Override + public PiiMessageBuilder withPayload(T payload) { + return new PiiMessageBuilder<>(payload, null); + } + + } + + private static class PiiMessageBuilder

extends BaseMessageBuilder> { + + public PiiMessageBuilder(P payload, @Nullable Message

originalMessage) { + super(payload, originalMessage); + } + + @Override + public Message

build() { + return new PiiMessage<>(getPayload(), getHeaders()); + } + + } + + private static class PiiMessage

extends GenericMessage

{ + + @Serial + private static final long serialVersionUID = -354503673433669578L; + + public PiiMessage(P payload, Map headers) { + super(payload, headers); + } + + @Override + public String toString() { + return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']'; + } + + private static Map maskHeaders(Map headers) { + return headers.entrySet() + .stream() + .map((entry) -> entry.getKey().equals("password") ? Map.entry(entry.getKey(), "******") : entry) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + } + } diff --git a/src/reference/antora/modules/ROOT/pages/message.adoc b/src/reference/antora/modules/ROOT/pages/message.adoc index 4405d304dcd..afadf8cb1d3 100644 --- a/src/reference/antora/modules/ROOT/pages/message.adoc +++ b/src/reference/antora/modules/ROOT/pages/message.adoc @@ -245,14 +245,14 @@ When messages are processed (and modified) by message-producing endpoints (such One exception to this is a xref:transformer.adoc[transformer], when a complete message is returned to the framework. In that case, the user code is responsible for the entire outbound message. When a transformer just returns the payload, the inbound headers are propagated. -Also, a header is only propagated if it does not already exist in the outbound message, letting you change header values as needed. +Also, a header only propagated if it does not already exist in the outbound message, letting you change header values as needed. Starting with version 4.3.10, you can configure message handlers (that modify messages and produce output) to suppress the propagation of specific headers. To configure the header(s) you do not want to be copied, call the `setNotPropagatedHeaders()` or `addNotPropagatedHeaders()` methods on the `MessageProducingMessageHandler` abstract class. You can also globally suppress propagation of specific message headers by setting the `readOnlyHeaders` property in `META-INF/spring.integration.properties` to a comma-delimited list of headers. -Starting with version 5.0, the `setNotPropagatedHeaders()` implementation on the `AbstractMessageProducingHandler` applies simple patterns (`xxx*`, `*xxx`, `*xxx*`, or `xxx*yyy`) to allow filtering headers with a common suffix or prefix. +Starting with version 5.0, the `setNotPropagatedHeaders()` implementation on the `AbstractMessageProducingHandler` applies simple patterns (`xxx*`, `\*xxx`, `*xxx*`, or `xxx*yyy`) to allow filtering headers with a common suffix or prefix. See https://docs.spring.io/spring-integration/api/org/springframework/integration/util/PatternMatchUtils.html[`PatternMatchUtils` Javadoc] for more information. When one of the patterns is `*` (asterisk), no headers are propagated. All other patterns are ignored. @@ -290,13 +290,15 @@ Throwable t = message.getPayload(); Note that this implementation takes advantage of the fact that the `GenericMessage` base class is parameterized. Therefore, as shown in both examples, no casting is necessary when retrieving the `Message` payload `Object`. +The mentioned `Message` class implementations are immutable. +In some cases, when mutability is not a concern and the logic of application is well-designed to avoid concurrent modifications, a `MutableMessage` can be used. + [[message-builder]] == The `MessageBuilder` Helper Class You may notice that the `Message` interface defines retrieval methods for its payload and headers but provides no setters. The reason for this is that a `Message` cannot be modified after its initial creation. -Therefore, when a `Message` instance is sent to multiple consumers (for example, -through a publish-subscribe Channel), if one of those consumers needs to send a reply with a different payload type, it must create a new `Message`. +Therefore, when a `Message` instance is sent to multiple consumers (for example, through a publish-subscribe Channel), if one of those consumers needs to send a reply with a different payload type, it must create a new `Message`. As a result, the other consumers are not affected by those changes. Keep in mind that multiple consumers may access the same payload instance or header value, and whether such an instance is itself immutable is a decision left to you. In other words, the contract for `Message` instances is similar to that of an unmodifiable `Collection`, and the `MessageHeaders` map further exemplifies that. @@ -359,3 +361,76 @@ assertEquals(2, lessImportantMessage.getHeaders().getPriority()); The `priority` header is considered only when using a `PriorityChannel` (as described in the next chapter). It is defined as a `java.lang.Integer`. + +The `MutableMessageBuilder` is provided to deal with `MutableMessage` instances. +The logic of this class is to create a `MutableMessage` or leave it as is and mutate its content via builder methods. +This way there is a slight performance gain in the running application, when immutability is not a concern of message exchanges. + +NOTE: Starting with version 6.4, a `BaseMessageBuilder` class is extracted from the `MessageBuilder` to simplify an extension for the default message building logic. +For example, together with a custom `MessageBuilderFactory`, a custom `BaseMessageBuilder` implementation could be used globally in the application context to provide custom `Message` instances. +In particular, the `GenericMessage.toString()` method can be overridden to hide sensitive information from payload and headers when such a message is logged. + +[[message-builder-factory]] +== The `MessageBuilderFactory` abstraction + +The `MessageBuilderFactory` bean with `IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME` is registered globally into an application context and used everywhere in the framework to create `Message` instances. +By default, it is an instance of `DefaultMessageBuilderFactory`. +Out of the box, the framework also provides a `MutableMessageBuilderFactory` to create `MutableMessage` instances in the framework components instead. +To customize `Message` instances creation, a `MessageBuilderFactory` bean with `IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME` has to be provided in the target application context to override a default one. +For example, a custom `MessageBuilderFactory` could be registered for an implementation of the `BaseMessageBuilder` where we would like to provide a `GenericMessage` extension with overridden `toString()` to to hide sensitive information from payload and headers when such a message is logged. + +Some quick implementation of these classes to demonstrate a personal identifiable information mitigation can be like this: +[source,java] +---- +class PiiMessageBuilderFactory implements MessageBuilderFactory { + + @Override + public PiiMessageBuilder fromMessage(Message message) { + return new PiiMessageBuilder<>(message.getPayload(), message); + } + + @Override + public PiiMessageBuilder withPayload(T payload) { + return new PiiMessageBuilder<>(payload, null); + } + +} + +class PiiMessageBuilder

extends BaseMessageBuilder> { + + public PiiMessageBuilder(P payload, @Nullable Message

originalMessage) { + super(payload, originalMessage); + } + + @Override + public Message

build() { + return new PiiMessage<>(getPayload(), getHeaders()); + } + +} + +class PiiMessage

extends GenericMessage

{ + + @Serial + private static final long serialVersionUID = -354503673433669578L; + + public PiiMessage(P payload, Map headers) { + super(payload, headers); + } + + @Override + public String toString() { + return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']'; + } + + private static Map maskHeaders(Map headers) { + return headers.entrySet() + .stream() + .map((entry) -> entry.getKey().equals("password") ? Map.entry(entry.getKey(), "******") : entry) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + +} +---- + +The this `PiiMessageBuilderFactory` could be registered as a bean, and whenever the framework logs the message (e.g. in case of `errorChannel`), the `password` header will be masked. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 7b19af0bf0a..4fba52705a9 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -16,6 +16,9 @@ In general the project has been moved to the latest dependency versions. [[x6.4-new-components]] === New Components +A `BaseMessageBuilder` class has been extracted from the `MessageBuilder` to simplify a custom builder implementation where the most of the logic should be the same as `MessageBuilder` one. +See xref:message.adoc#message-builder[`MessageBuilder`] for more information. + The new Control Bus interaction model is implemented in the `ControlBusCommandRegistry`. A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`. See xref:control-bus.adoc[Control Bus] for more information.