INT-4446 Improve EmbeddedJsonHeadersMessageMapper (#2422)
* INT-4446 Improve EmbeddedJsonHeadersMessageMapper JIRA: https://jira.spring.io/browse/INT-4446 * Do not recreate message if not necessarily * Do not let to generate `id` and `timestamp` if they are not mapped * Use `smartMatch` to allow to configure negative patterns * Introduce `PatternMatchUtils.smartMatchIgnoreCase()` for convenience **Cherry-pick to 5.0.x** * * Polishing `EmbeddedJsonHeadersMessageMapper`
This commit is contained in:
committed by
Gary Russell
parent
15fc23c2fa
commit
7bcf6a040a
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2017-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,7 @@ package org.springframework.integration.support.json;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -31,10 +29,11 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.integration.mapping.BytesMessageMapper;
|
||||
import org.springframework.integration.support.MutableMessage;
|
||||
import org.springframework.integration.support.MutableMessageHeaders;
|
||||
import org.springframework.integration.support.utils.PatternMatchUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -82,7 +81,7 @@ public class EmbeddedJsonHeadersMessageMapper implements BytesMessageMapper {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final List<String> headerPatterns;
|
||||
private final String[] headerPatterns;
|
||||
|
||||
private final boolean allHeaders;
|
||||
|
||||
@@ -102,7 +101,7 @@ public class EmbeddedJsonHeadersMessageMapper implements BytesMessageMapper {
|
||||
* Construct an instance that embeds headers matching the supplied patterns, using
|
||||
* the default JSON object mapper.
|
||||
* @param headerPatterns the patterns.
|
||||
* @see PatternMatchUtils#simpleMatch(String, String)
|
||||
* @see PatternMatchUtils#smartMatch(String, String...)
|
||||
*/
|
||||
public EmbeddedJsonHeadersMessageMapper(String... headerPatterns) {
|
||||
this(JacksonJsonUtils.messagingAwareMapper(), headerPatterns);
|
||||
@@ -125,8 +124,8 @@ public class EmbeddedJsonHeadersMessageMapper implements BytesMessageMapper {
|
||||
*/
|
||||
public EmbeddedJsonHeadersMessageMapper(ObjectMapper objectMapper, String... headerPatterns) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.headerPatterns = Arrays.asList(headerPatterns);
|
||||
this.allHeaders = this.headerPatterns.size() == 1 && this.headerPatterns.get(0).equals("*");
|
||||
this.headerPatterns = Arrays.copyOf(headerPatterns, headerPatterns.length);
|
||||
this.allHeaders = this.headerPatterns.length == 1 && this.headerPatterns[0].equals("*");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,35 +150,54 @@ public class EmbeddedJsonHeadersMessageMapper implements BytesMessageMapper {
|
||||
}
|
||||
|
||||
public Collection<String> getHeaderPatterns() {
|
||||
return Collections.unmodifiableCollection(this.headerPatterns);
|
||||
return Arrays.asList(this.headerPatterns);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public byte[] fromMessage(Message<?> message) throws Exception {
|
||||
Message<?> messageToEncode = this.allHeaders ? message : pruneHeaders(message);
|
||||
Map<String, Object> headersToEncode =
|
||||
this.allHeaders
|
||||
? message.getHeaders()
|
||||
: pruneHeaders(message.getHeaders());
|
||||
|
||||
if (this.rawBytes && message.getPayload() instanceof byte[]) {
|
||||
return fromBytesPayload((Message<byte[]>) messageToEncode);
|
||||
return fromBytesPayload((byte[]) message.getPayload(), headersToEncode);
|
||||
}
|
||||
else {
|
||||
Message<?> messageToEncode = message;
|
||||
|
||||
if (!this.allHeaders) {
|
||||
if (!headersToEncode.containsKey(MessageHeaders.ID)) {
|
||||
headersToEncode.put(MessageHeaders.ID, MessageHeaders.ID_VALUE_NONE);
|
||||
}
|
||||
if (!headersToEncode.containsKey(MessageHeaders.TIMESTAMP)) {
|
||||
headersToEncode.put(MessageHeaders.TIMESTAMP, -1L);
|
||||
}
|
||||
|
||||
messageToEncode = new MutableMessage<>(message.getPayload(), headersToEncode);
|
||||
}
|
||||
|
||||
return this.objectMapper.writeValueAsBytes(messageToEncode);
|
||||
}
|
||||
}
|
||||
|
||||
private Message<?> pruneHeaders(Message<?> message) {
|
||||
Map<String, Object> headersToEmbed =
|
||||
message.getHeaders().entrySet().stream()
|
||||
.filter(e -> this.headerPatterns.stream().anyMatch(p ->
|
||||
this.caseSensitive
|
||||
? PatternMatchUtils.simpleMatch(p, e.getKey())
|
||||
: PatternMatchUtils.simpleMatch(p.toLowerCase(), e.getKey().toLowerCase())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
return new MutableMessage<>(message.getPayload(), headersToEmbed);
|
||||
private Map<String, Object> pruneHeaders(MessageHeaders messageHeaders) {
|
||||
return messageHeaders
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> matchHeader(e.getKey()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
private byte[] fromBytesPayload(Message<byte[]> message) throws Exception {
|
||||
byte[] headers = this.objectMapper.writeValueAsBytes(message.getHeaders());
|
||||
byte[] payload = message.getPayload();
|
||||
private boolean matchHeader(String header) {
|
||||
return Boolean.TRUE.equals(this.caseSensitive
|
||||
? PatternMatchUtils.smartMatch(header, this.headerPatterns)
|
||||
: PatternMatchUtils.smartMatchIgnoreCase(header, this.headerPatterns));
|
||||
}
|
||||
|
||||
private byte[] fromBytesPayload(byte[] payload, Map<String, Object> headersToEncode) throws Exception {
|
||||
byte[] headers = this.objectMapper.writeValueAsBytes(headersToEncode);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(new byte[8 + headers.length + payload.length]);
|
||||
buffer.putInt(headers.length);
|
||||
buffer.put(headers);
|
||||
@@ -189,7 +207,7 @@ public class EmbeddedJsonHeadersMessageMapper implements BytesMessageMapper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> toMessage(byte[] bytes, @Nullable Map<String, Object> headers) throws Exception {
|
||||
public Message<?> toMessage(byte[] bytes, @Nullable Map<String, Object> headers) {
|
||||
Message<?> message = null;
|
||||
try {
|
||||
message = decodeNativeFormat(bytes, headers);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2017-2018 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,12 +16,15 @@
|
||||
|
||||
package org.springframework.integration.support.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Utility methods for pattern matching.
|
||||
* This utilities provide support of negative pattern matching as well
|
||||
* unlike {@link org.springframework.util.PatternMatchUtils}.
|
||||
*
|
||||
* @author Meherzad Lahewala
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 5.0
|
||||
*
|
||||
@@ -31,6 +34,28 @@ public final class PatternMatchUtils {
|
||||
|
||||
private PatternMatchUtils() { }
|
||||
|
||||
/**
|
||||
* Pattern match against the supplied patterns ignoring case; also supports negated ('!')
|
||||
* patterns. First match wins (positive or negative).
|
||||
* To match the names starting with {@code !} symbol,
|
||||
* you have to escape it prepending with the {@code \} symbol in the pattern definition.
|
||||
* @param str the string to match.
|
||||
* @param patterns the patterns.
|
||||
* @return true for positive match; false for negative; null if no pattern matches.
|
||||
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String[], String)
|
||||
* @since 5.0.5
|
||||
*/
|
||||
public static Boolean smartMatchIgnoreCase(String str, String... patterns) {
|
||||
if (patterns != null) {
|
||||
return smartMatch(str.toLowerCase(),
|
||||
Arrays.stream(patterns)
|
||||
.map(String::toLowerCase)
|
||||
.toArray(String[]::new));
|
||||
}
|
||||
|
||||
return null; //NOSONAR - intentional null return
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern match against the supplied patterns; also supports negated ('!')
|
||||
* patterns. First match wins (positive or negative).
|
||||
@@ -58,6 +83,7 @@ public final class PatternMatchUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; //NOSONAR - intentional null return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* Copyright 2017-2018 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,21 +16,25 @@
|
||||
|
||||
package org.springframework.integration.support.json;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 5.0
|
||||
*
|
||||
*/
|
||||
@@ -40,17 +44,31 @@ public class EmbeddedJsonHeadersMessageMapperTests {
|
||||
public void testEmbedAll() throws Exception {
|
||||
EmbeddedJsonHeadersMessageMapper mapper = new EmbeddedJsonHeadersMessageMapper();
|
||||
GenericMessage<String> message = new GenericMessage<>("foo");
|
||||
assertThat(mapper.toMessage(mapper.fromMessage(message)), equalTo(message));
|
||||
|
||||
assertThat(mapper.toMessage(mapper.fromMessage(message))).isEqualTo(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmbedSome() throws Exception {
|
||||
EmbeddedJsonHeadersMessageMapper mapper = new EmbeddedJsonHeadersMessageMapper("id");
|
||||
EmbeddedJsonHeadersMessageMapper mapper = new EmbeddedJsonHeadersMessageMapper(MessageHeaders.ID);
|
||||
GenericMessage<String> message = new GenericMessage<>("foo");
|
||||
Message<?> decoded = mapper.toMessage(mapper.fromMessage(message));
|
||||
assertThat(decoded.getPayload(), equalTo(message.getPayload()));
|
||||
assertThat(decoded.getHeaders().getId(), equalTo(message.getHeaders().getId()));
|
||||
byte[] encodedMessage = mapper.fromMessage(message);
|
||||
Message<?> decoded = mapper.toMessage(encodedMessage);
|
||||
assertThat(decoded.getPayload()).isEqualTo(message.getPayload());
|
||||
assertThat(decoded.getHeaders().getTimestamp()).isNotEqualTo(message.getHeaders().getTimestamp());
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, Object> encodedMessageToCheck =
|
||||
objectMapper.readValue(encodedMessage, new TypeReference<Map<String, Object>>() {
|
||||
|
||||
});
|
||||
|
||||
Object headers = encodedMessageToCheck.get("headers");
|
||||
assertThat(headers).isNotNull();
|
||||
assertThat(headers).isInstanceOf(Map.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> headersToCheck = (Map<String, Object>) headers;
|
||||
assertThat(headersToCheck).doesNotContainKey(MessageHeaders.TIMESTAMP);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,13 +82,13 @@ public class EmbeddedJsonHeadersMessageMapperTests {
|
||||
byte[] headerBytes = new byte[headerLen];
|
||||
bb.get(headerBytes);
|
||||
String headers = new String(headerBytes);
|
||||
assertThat(headers, containsString(message.getHeaders().getId().toString()));
|
||||
assertThat(headers, containsString(String.valueOf(message.getHeaders().getTimestamp())));
|
||||
assertThat(bb.getInt(), equalTo(3));
|
||||
assertThat(bb.remaining(), equalTo(3));
|
||||
assertThat((char) bb.get(), equalTo('f'));
|
||||
assertThat((char) bb.get(), equalTo('o'));
|
||||
assertThat((char) bb.get(), equalTo('o'));
|
||||
assertThat(headers).contains(message.getHeaders().getId().toString());
|
||||
assertThat(headers).contains(String.valueOf(message.getHeaders().getTimestamp()));
|
||||
assertThat(bb.getInt()).isEqualTo(3);
|
||||
assertThat(bb.remaining()).isEqualTo(3);
|
||||
assertThat((char) bb.get()).isEqualTo('f');
|
||||
assertThat((char) bb.get()).isEqualTo('o');
|
||||
assertThat((char) bb.get()).isEqualTo('o');
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,13 +101,14 @@ public class EmbeddedJsonHeadersMessageMapperTests {
|
||||
byte[] headerBytes = new byte[headerLen];
|
||||
bb.get(headerBytes);
|
||||
String headers = new String(headerBytes);
|
||||
assertThat(headers, containsString(message.getHeaders().getId().toString()));
|
||||
assertThat(headers, not(containsString("bar")));
|
||||
assertThat(bb.getInt(), equalTo(3));
|
||||
assertThat(bb.remaining(), equalTo(3));
|
||||
assertThat((char) bb.get(), equalTo('f'));
|
||||
assertThat((char) bb.get(), equalTo('o'));
|
||||
assertThat((char) bb.get(), equalTo('o'));
|
||||
assertThat(headers).contains(message.getHeaders().getId().toString());
|
||||
assertThat(headers).doesNotContain(MessageHeaders.TIMESTAMP);
|
||||
assertThat(headers).doesNotContain("bar");
|
||||
assertThat(bb.getInt()).isEqualTo(3);
|
||||
assertThat(bb.remaining()).isEqualTo(3);
|
||||
assertThat((char) bb.get()).isEqualTo('f');
|
||||
assertThat((char) bb.get()).isEqualTo('o');
|
||||
assertThat((char) bb.get()).isEqualTo('o');
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,10 +118,10 @@ public class EmbeddedJsonHeadersMessageMapperTests {
|
||||
GenericMessage<byte[]> message = new GenericMessage<>("foo".getBytes());
|
||||
byte[] mappedBytes = mapper.fromMessage(message);
|
||||
String mapped = new String(mappedBytes);
|
||||
assertThat(mapped, containsString("[B\",\"Zm9v"));
|
||||
assertThat(mapped).contains("[B\",\"Zm9v");
|
||||
@SuppressWarnings("unchecked")
|
||||
Message<byte[]> decoded = (Message<byte[]>) mapper.toMessage(mappedBytes);
|
||||
assertThat(new String(decoded.getPayload()), equalTo("foo"));
|
||||
assertThat(new String(decoded.getPayload())).isEqualTo("foo");
|
||||
|
||||
}
|
||||
|
||||
@@ -111,7 +130,35 @@ public class EmbeddedJsonHeadersMessageMapperTests {
|
||||
EmbeddedJsonHeadersMessageMapper mapper = new EmbeddedJsonHeadersMessageMapper();
|
||||
GenericMessage<byte[]> message = new GenericMessage<>("foo".getBytes());
|
||||
Message<?> decoded = mapper.toMessage(mapper.fromMessage(message));
|
||||
assertThat(decoded, equalTo(message));
|
||||
assertThat(decoded).isEqualTo(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDontMapIdButOthers() throws Exception {
|
||||
EmbeddedJsonHeadersMessageMapper mapper = new EmbeddedJsonHeadersMessageMapper("!" + MessageHeaders.ID, "*");
|
||||
GenericMessage<String> message = new GenericMessage<>("foo", Collections.singletonMap("bar", "baz"));
|
||||
byte[] encodedMessage = mapper.fromMessage(message);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, Object> encodedMessageToCheck =
|
||||
objectMapper.readValue(encodedMessage, new TypeReference<Map<String, Object>>() {
|
||||
|
||||
});
|
||||
|
||||
Object headers = encodedMessageToCheck.get("headers");
|
||||
assertThat(headers).isNotNull();
|
||||
assertThat(headers).isInstanceOf(Map.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> headersToCheck = (Map<String, Object>) headers;
|
||||
assertThat(headersToCheck).doesNotContainKey(MessageHeaders.ID);
|
||||
assertThat(headersToCheck).containsKey(MessageHeaders.TIMESTAMP);
|
||||
assertThat(headersToCheck).containsKey("bar");
|
||||
|
||||
Message<?> decoded = mapper.toMessage(mapper.fromMessage(message));
|
||||
assertThat(decoded.getHeaders().getTimestamp()).isEqualTo(message.getHeaders().getTimestamp());
|
||||
assertThat(decoded.getHeaders().getId()).isNotEqualTo(message.getHeaders().getId());
|
||||
assertThat(decoded.getHeaders().get("bar")).isEqualTo("baz");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user