Skip to content

Commit f87aff3

Browse files
committed
GH-9416: Extract BaseMessageBuilder for easier message extensions
Fixes: #9416 Issue link: #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
1 parent 4ee5532 commit f87aff3

File tree

6 files changed

+497
-285
lines changed

6 files changed

+497
-285
lines changed

spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public IntegrationMessageHeaderAccessor(@Nullable Message<?> message) {
108108
* @see #isReadOnly(String)
109109
*/
110110
public void setReadOnlyHeaders(String... readOnlyHeaders) {
111-
Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not be contain null items.");
111+
Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not contain null items.");
112112
if (!ObjectUtils.isEmpty(readOnlyHeaders)) {
113113
this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders));
114114
}
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.support;
18+
19+
import java.util.Arrays;
20+
import java.util.Date;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.integration.IntegrationMessageHeaderAccessor;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.messaging.Message;
30+
import org.springframework.messaging.MessageChannel;
31+
import org.springframework.messaging.MessageHeaders;
32+
import org.springframework.messaging.support.ErrorMessage;
33+
import org.springframework.messaging.support.GenericMessage;
34+
import org.springframework.util.Assert;
35+
import org.springframework.util.ObjectUtils;
36+
37+
/**
38+
* The {@link AbstractIntegrationMessageBuilder} extension for the default logic to build message.
39+
* The {@link MessageBuilder} is fully based on this class.
40+
* This abstract class can be used for creating custom {@link Message} instances.
41+
* For that purpose its {@link #build()} method has to be overridden.
42+
* The custom {@link Message} type could be used, for example, to hide sensitive information
43+
* from payload and headers when message is logged.
44+
* For this goal there would be enough to override {@link GenericMessage#toString()}
45+
* and filter out (or mask) those headers which container such sensitive information.
46+
*
47+
* @param <T> the payload type.
48+
* @param <B> the target builder class type.
49+
*
50+
* @author Artem Bilan
51+
*
52+
* @since 6.4
53+
*
54+
* @see MessageBuilder
55+
* @see MessageBuilderFactory
56+
*/
57+
public abstract class BaseMessageBuilder<T, B extends BaseMessageBuilder<T, B>>
58+
extends AbstractIntegrationMessageBuilder<T> {
59+
60+
private static final Log LOGGER = LogFactory.getLog(BaseMessageBuilder.class);
61+
62+
private final T payload;
63+
64+
private final IntegrationMessageHeaderAccessor headerAccessor;
65+
66+
@Nullable
67+
private final Message<T> originalMessage;
68+
69+
private volatile boolean modified;
70+
71+
private String[] readOnlyHeaders;
72+
73+
protected BaseMessageBuilder(T payload, @Nullable Message<T> originalMessage) {
74+
Assert.notNull(payload, "payload must not be null");
75+
this.payload = payload;
76+
this.originalMessage = originalMessage;
77+
this.headerAccessor = new IntegrationMessageHeaderAccessor(originalMessage);
78+
if (originalMessage != null) {
79+
this.modified = (!this.payload.equals(originalMessage.getPayload()));
80+
}
81+
}
82+
83+
@Override
84+
public T getPayload() {
85+
return this.payload;
86+
}
87+
88+
@Override
89+
public Map<String, Object> getHeaders() {
90+
return this.headerAccessor.toMap();
91+
}
92+
93+
@Nullable
94+
@Override
95+
public <V> V getHeader(String key, Class<V> type) {
96+
return this.headerAccessor.getHeader(key, type);
97+
}
98+
99+
/**
100+
* Set the value for the given header name. If the provided value is {@code null}, the header will be removed.
101+
* @param headerName The header name.
102+
* @param headerValue The header value.
103+
* @return this MessageBuilder.
104+
*/
105+
@Override
106+
public B setHeader(String headerName, @Nullable Object headerValue) {
107+
this.headerAccessor.setHeader(headerName, headerValue);
108+
return _this();
109+
}
110+
111+
/**
112+
* Set the value for the given header name only if the header name is not already associated with a value.
113+
* @param headerName The header name.
114+
* @param headerValue The header value.
115+
* @return this MessageBuilder.
116+
*/
117+
@Override
118+
public B setHeaderIfAbsent(String headerName, Object headerValue) {
119+
this.headerAccessor.setHeaderIfAbsent(headerName, headerValue);
120+
return _this();
121+
}
122+
123+
/**
124+
* Removes all headers provided via array of 'headerPatterns'. As the name suggests the array
125+
* may contain simple matching patterns for header names. Supported pattern styles are:
126+
* {@code xxx*}, {@code *xxx}, {@code *xxx*} and {@code xxx*yyy}.
127+
* @param headerPatterns The header patterns.
128+
* @return this MessageBuilder.
129+
*/
130+
@Override
131+
public B removeHeaders(String... headerPatterns) {
132+
this.headerAccessor.removeHeaders(headerPatterns);
133+
return _this();
134+
}
135+
136+
/**
137+
* Remove the value for the given header name.
138+
* @param headerName The header name.
139+
* @return this MessageBuilder.
140+
*/
141+
@Override
142+
public B removeHeader(String headerName) {
143+
if (!this.headerAccessor.isReadOnly(headerName)) {
144+
this.headerAccessor.removeHeader(headerName);
145+
}
146+
else if (LOGGER.isInfoEnabled()) {
147+
LOGGER.info("The header [" + headerName + "] is ignored for removal because it is is readOnly.");
148+
}
149+
return _this();
150+
}
151+
152+
/**
153+
* Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use
154+
* {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values
155+
* will never be overwritten.
156+
* @param headersToCopy The headers to copy.
157+
* @return this MessageBuilder.
158+
* @see MessageHeaders#ID
159+
* @see MessageHeaders#TIMESTAMP
160+
*/
161+
@Override
162+
public B copyHeaders(@Nullable Map<String, ?> headersToCopy) {
163+
this.headerAccessor.copyHeaders(headersToCopy);
164+
return _this();
165+
}
166+
167+
/**
168+
* Copy the name-value pairs from the provided Map. This operation will not override any existing values.
169+
* @param headersToCopy The headers to copy.
170+
* @return this MessageBuilder.
171+
*/
172+
@Override
173+
public B copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
174+
if (headersToCopy != null) {
175+
for (Map.Entry<String, ?> entry : headersToCopy.entrySet()) {
176+
String headerName = entry.getKey();
177+
if (!this.headerAccessor.isReadOnly(headerName)) {
178+
this.headerAccessor.setHeaderIfAbsent(headerName, entry.getValue());
179+
}
180+
}
181+
}
182+
return _this();
183+
}
184+
185+
@SuppressWarnings("unchecked")
186+
@Override
187+
@Nullable
188+
protected List<List<Object>> getSequenceDetails() {
189+
return (List<List<Object>>) this.headerAccessor.getHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
190+
}
191+
192+
@Override
193+
@Nullable
194+
protected Object getCorrelationId() {
195+
return this.headerAccessor.getCorrelationId();
196+
}
197+
198+
@Override
199+
protected Object getSequenceNumber() {
200+
return this.headerAccessor.getSequenceNumber();
201+
}
202+
203+
@Override
204+
protected Object getSequenceSize() {
205+
return this.headerAccessor.getSequenceSize();
206+
}
207+
208+
@Override
209+
public B pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) {
210+
super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize);
211+
return _this();
212+
}
213+
214+
@Override
215+
public B popSequenceDetails() {
216+
super.popSequenceDetails();
217+
return _this();
218+
}
219+
220+
@Override
221+
public B setExpirationDate(@Nullable Long expirationDate) {
222+
super.setExpirationDate(expirationDate);
223+
return _this();
224+
}
225+
226+
@Override
227+
public B setExpirationDate(@Nullable Date expirationDate) {
228+
super.setExpirationDate(expirationDate);
229+
return _this();
230+
}
231+
232+
@Override
233+
public B setCorrelationId(Object correlationId) {
234+
super.setCorrelationId(correlationId);
235+
return _this();
236+
}
237+
238+
@Override
239+
public B setReplyChannel(MessageChannel replyChannel) {
240+
super.setReplyChannel(replyChannel);
241+
return _this();
242+
}
243+
244+
@Override
245+
public B setReplyChannelName(String replyChannelName) {
246+
super.setReplyChannelName(replyChannelName);
247+
return _this();
248+
}
249+
250+
@Override
251+
public B setErrorChannel(MessageChannel errorChannel) {
252+
super.setErrorChannel(errorChannel);
253+
return _this();
254+
}
255+
256+
@Override
257+
public B setErrorChannelName(String errorChannelName) {
258+
super.setErrorChannelName(errorChannelName);
259+
return _this();
260+
}
261+
262+
@Override
263+
public B setSequenceNumber(Integer sequenceNumber) {
264+
super.setSequenceNumber(sequenceNumber);
265+
return _this();
266+
}
267+
268+
@Override
269+
public B setSequenceSize(Integer sequenceSize) {
270+
super.setSequenceSize(sequenceSize);
271+
return _this();
272+
}
273+
274+
@Override
275+
public B setPriority(Integer priority) {
276+
super.setPriority(priority);
277+
return _this();
278+
}
279+
280+
/**
281+
* Specify a list of headers which should be considered as read only
282+
* and prohibited from being populated in the message.
283+
* @param readOnlyHeaders the list of headers for {@code readOnly} mode.
284+
* Defaults to {@link MessageHeaders#ID} and {@link MessageHeaders#TIMESTAMP}.
285+
* @return the current {@link BaseMessageBuilder}
286+
* @see IntegrationMessageHeaderAccessor#isReadOnly(String)
287+
*/
288+
public B readOnlyHeaders(@Nullable String... readOnlyHeaders) {
289+
this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null;
290+
if (readOnlyHeaders != null) {
291+
this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders);
292+
}
293+
return _this();
294+
}
295+
296+
/**
297+
* Return an original message instance if it is not modified and does not have read-only headers.
298+
* If payload is an instance of {@link Throwable}, then an {@link ErrorMessage} is built.
299+
* Otherwise, a new instance of {@link GenericMessage} is produced.
300+
* This method can be overridden to provide any custom message implementations.
301+
* @return the message instance
302+
* @see #getPayload()
303+
* @see #getHeaders()
304+
*/
305+
@Override
306+
@SuppressWarnings("unchecked")
307+
public Message<T> build() {
308+
if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null
309+
&& !containsReadOnly(this.originalMessage.getHeaders())) {
310+
311+
return this.originalMessage;
312+
}
313+
if (payload instanceof Throwable throwable) {
314+
return (Message<T>) new ErrorMessage(throwable, getHeaders());
315+
}
316+
return new GenericMessage<>(payload, getHeaders());
317+
}
318+
319+
private boolean containsReadOnly(MessageHeaders headers) {
320+
if (!ObjectUtils.isEmpty(this.readOnlyHeaders)) {
321+
for (String readOnly : this.readOnlyHeaders) {
322+
if (headers.containsKey(readOnly)) {
323+
return true;
324+
}
325+
}
326+
}
327+
return false;
328+
}
329+
330+
@SuppressWarnings("unchecked")
331+
private B _this() {
332+
return (B) this;
333+
}
334+
335+
}

0 commit comments

Comments
 (0)