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:
Artem Bilan
2018-04-19 13:22:40 -04:00
committed by Gary Russell
parent 15fc23c2fa
commit 7bcf6a040a
3 changed files with 144 additions and 53 deletions

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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");
}
}