GH-9416: Extract BaseMessageBuilder for easier message extensions
Fixes: #9416 Issue link: https://github.com/spring-projects/spring-integration/issues/9416 The `MessageBuilderFactory` bean could be used a central place to provide custom `Message` implementation into the application. For example, the `GenericMessage.toString()` can be overridden to remove or mask sensitive information from the payload or headers. * Extract a `BaseMessageBuilder` from the `MessageBuilder` class to simplify a custom `MessageBuilderFactory` implementation * Test and document new feature and its purpose
This commit is contained in:
@@ -108,7 +108,7 @@ public class IntegrationMessageHeaderAccessor extends MessageHeaderAccessor {
|
||||
* @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));
|
||||
}
|
||||
|
||||
@@ -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 <T> the payload type.
|
||||
* @param <B> the target builder class type.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*
|
||||
* @see MessageBuilder
|
||||
* @see MessageBuilderFactory
|
||||
*/
|
||||
public abstract class BaseMessageBuilder<T, B extends BaseMessageBuilder<T, B>>
|
||||
extends AbstractIntegrationMessageBuilder<T> {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(BaseMessageBuilder.class);
|
||||
|
||||
private final T payload;
|
||||
|
||||
private final IntegrationMessageHeaderAccessor headerAccessor;
|
||||
|
||||
@Nullable
|
||||
private final Message<T> originalMessage;
|
||||
|
||||
private volatile boolean modified;
|
||||
|
||||
private String[] readOnlyHeaders;
|
||||
|
||||
protected BaseMessageBuilder(T payload, @Nullable Message<T> 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<String, Object> getHeaders() {
|
||||
return this.headerAccessor.toMap();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V getHeader(String key, Class<V> 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<String, ?> 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<String, ?> headersToCopy) {
|
||||
if (headersToCopy != null) {
|
||||
for (Map.Entry<String, ?> 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<List<Object>> getSequenceDetails() {
|
||||
return (List<List<Object>>) 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<T> build() {
|
||||
if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null
|
||||
&& !containsReadOnly(this.originalMessage.getHeaders())) {
|
||||
|
||||
return this.originalMessage;
|
||||
}
|
||||
if (payload instanceof Throwable throwable) {
|
||||
return (Message<T>) 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 @@ import org.springframework.util.ObjectUtils;
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T> {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(MessageBuilder.class);
|
||||
|
||||
private final T payload;
|
||||
|
||||
private final IntegrationMessageHeaderAccessor headerAccessor;
|
||||
|
||||
@Nullable
|
||||
private final Message<T> originalMessage;
|
||||
|
||||
private volatile boolean modified;
|
||||
|
||||
private String[] readOnlyHeaders;
|
||||
public final class MessageBuilder<T> extends BaseMessageBuilder<T, MessageBuilder<T>> {
|
||||
|
||||
/**
|
||||
* Private constructor to be invoked from the static factory methods only.
|
||||
*/
|
||||
private MessageBuilder(T payload, @Nullable Message<T> 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<String, Object> getHeaders() {
|
||||
return this.headerAccessor.toMap();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V getHeader(String key, Class<V> 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 <T> The type of the payload.
|
||||
@@ -114,230 +66,4 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
|
||||
return new MessageBuilder<>(payload, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for the given header name. If the provided value is <code>null</code>, the header will be removed.
|
||||
* @param headerName The header name.
|
||||
* @param headerValue The header value.
|
||||
* @return this MessageBuilder.
|
||||
*/
|
||||
@Override
|
||||
public MessageBuilder<T> 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<T> setHeaderIfAbsent(String headerName, Object headerValue) {
|
||||
this.headerAccessor.setHeaderIfAbsent(headerName, headerValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all headers provided via array of 'headerPatterns'. As the name suggests the array
|
||||
* may contain simple matching patterns for header names. Supported pattern styles are:
|
||||
* "xxx*", "*xxx", "*xxx*" and "xxx*yyy".
|
||||
* @param headerPatterns The header patterns.
|
||||
* @return this MessageBuilder.
|
||||
*/
|
||||
@Override
|
||||
public MessageBuilder<T> 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<T> 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<T> copyHeaders(@Nullable Map<String, ?> headersToCopy) {
|
||||
this.headerAccessor.copyHeaders(headersToCopy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the name-value pairs from the provided Map. This operation will <em>not</em> overwrite any existing values.
|
||||
* @param headersToCopy The headers to copy.
|
||||
* @return this MessageBuilder.
|
||||
*/
|
||||
@Override
|
||||
public MessageBuilder<T> copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
|
||||
if (headersToCopy != null) {
|
||||
for (Map.Entry<String, ?> 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<List<Object>> getSequenceDetails() {
|
||||
return (List<List<Object>>) 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<T> pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) {
|
||||
super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> popSequenceDetails() {
|
||||
super.popSequenceDetails();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setExpirationDate(@Nullable Long expirationDate) {
|
||||
super.setExpirationDate(expirationDate);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setExpirationDate(@Nullable Date expirationDate) {
|
||||
super.setExpirationDate(expirationDate);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setCorrelationId(Object correlationId) {
|
||||
super.setCorrelationId(correlationId);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setReplyChannel(MessageChannel replyChannel) {
|
||||
super.setReplyChannel(replyChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setReplyChannelName(String replyChannelName) {
|
||||
super.setReplyChannelName(replyChannelName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setErrorChannel(MessageChannel errorChannel) {
|
||||
super.setErrorChannel(errorChannel);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setErrorChannelName(String errorChannelName) {
|
||||
super.setErrorChannelName(errorChannelName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setSequenceNumber(Integer sequenceNumber) {
|
||||
super.setSequenceNumber(sequenceNumber);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> setSequenceSize(Integer sequenceSize) {
|
||||
super.setSequenceSize(sequenceSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageBuilder<T> 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<T> readOnlyHeaders(String... readOnlyHeaders) {
|
||||
this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null;
|
||||
this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Message<T> build() {
|
||||
if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null
|
||||
&& !containsReadOnly(this.originalMessage.getHeaders())) {
|
||||
|
||||
return this.originalMessage;
|
||||
}
|
||||
if (this.payload instanceof Throwable) {
|
||||
return (Message<T>) 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 class MessageBuilderTests {
|
||||
assertThat(message.getHeaders().get("qux")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void personalInfoHeadersAreMaskedWithCustomMessage() {
|
||||
Message<String> message =
|
||||
MessageBuilder.withPayload("some_user")
|
||||
.setHeader("password", "some_password")
|
||||
.build();
|
||||
|
||||
Message<String> 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 <T> PiiMessageBuilder<T> fromMessage(Message<T> message) {
|
||||
return new PiiMessageBuilder<>(message.getPayload(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> PiiMessageBuilder<T> withPayload(T payload) {
|
||||
return new PiiMessageBuilder<>(payload, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class PiiMessageBuilder<P> extends BaseMessageBuilder<P, PiiMessageBuilder<P>> {
|
||||
|
||||
public PiiMessageBuilder(P payload, @Nullable Message<P> originalMessage) {
|
||||
super(payload, originalMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<P> build() {
|
||||
return new PiiMessage<>(getPayload(), getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class PiiMessage<P> extends GenericMessage<P> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -354503673433669578L;
|
||||
|
||||
public PiiMessage(P payload, Map<String, Object> headers) {
|
||||
super(payload, headers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']';
|
||||
}
|
||||
|
||||
private static Map<String, Object> maskHeaders(Map<String, Object> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <T> PiiMessageBuilder<T> fromMessage(Message<T> message) {
|
||||
return new PiiMessageBuilder<>(message.getPayload(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> PiiMessageBuilder<T> withPayload(T payload) {
|
||||
return new PiiMessageBuilder<>(payload, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PiiMessageBuilder<P> extends BaseMessageBuilder<P, PiiMessageBuilder<P>> {
|
||||
|
||||
public PiiMessageBuilder(P payload, @Nullable Message<P> originalMessage) {
|
||||
super(payload, originalMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<P> build() {
|
||||
return new PiiMessage<>(getPayload(), getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PiiMessage<P> extends GenericMessage<P> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -354503673433669578L;
|
||||
|
||||
public PiiMessage(P payload, Map<String, Object> headers) {
|
||||
super(payload, headers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']';
|
||||
}
|
||||
|
||||
private static Map<String, Object> maskHeaders(Map<String, Object> 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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user