diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonIndexAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonIndexAccessor.java new file mode 100644 index 0000000000..3412486293 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonIndexAccessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025-present 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.json; + +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.node.ArrayNode; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.IndexAccessor; +import org.springframework.expression.TypedValue; + +/** + * A SpEL {@link IndexAccessor} that knows how to read indexes from JSON arrays, using + * Jackson's {@link ArrayNode} API. + * + *

Supports indexes supplied as an integer literal — for example, {@code myJsonArray[1]}. + * Also supports negative indexes — for example, {@code myJsonArray[-1]} which equates + * to {@code myJsonArray[myJsonArray.length - 1]}. Furthermore, {@code null} is returned for + * any index that is out of bounds (see {@link ArrayNode#get(int)} for details). + * + * @author Jooyoung Pyoung + * + * @since 7.0 + * @see JacksonPropertyAccessor + */ +public class JacksonIndexAccessor implements IndexAccessor { + + private static final Class[] SUPPORTED_CLASSES = { ArrayNode.class }; + + @Override + public Class[] getSpecificTargetClasses() { + return SUPPORTED_CLASSES; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, Object index) { + return (target instanceof ArrayNode && index instanceof Integer); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException { + ArrayNode arrayNode = (ArrayNode) target; + Integer intIndex = (Integer) index; + if (intIndex < 0) { + // negative index: get from the end of array, for compatibility with JacksonPropertyAccessor.ArrayNodeAsList. + intIndex = arrayNode.size() + intIndex; + } + return JacksonPropertyAccessor.typedValue(arrayNode.get(intIndex)); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, Object index) { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) { + throw new UnsupportedOperationException("Write is not supported"); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonPropertyAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonPropertyAccessor.java new file mode 100644 index 0000000000..17be286db6 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JacksonPropertyAccessor.java @@ -0,0 +1,314 @@ +/* + * Copyright 2025-present 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.json; + +import java.util.AbstractList; +import java.util.Iterator; + +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.NullNode; + +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.PropertyAccessor; +import org.springframework.expression.TypedValue; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A SpEL {@link PropertyAccessor} that knows how to read properties from JSON objects. + *

Uses Jackson {@link JsonNode} API for nested properties access. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + * @see JacksonIndexAccessor + */ +public class JacksonPropertyAccessor implements PropertyAccessor { + + /** + * The kind of types this can work with. + */ + private static final Class[] SUPPORTED_CLASSES = + { + String.class, + JsonNodeWrapper.class, + JsonNode.class + }; + + private ObjectMapper objectMapper = JsonMapper.builder() + .findAndAddModules(JacksonPropertyAccessor.class.getClassLoader()) + .build(); + + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "'objectMapper' cannot be null"); + this.objectMapper = objectMapper; + } + + @Override + public Class[] getSpecificTargetClasses() { + return SUPPORTED_CLASSES; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + JsonNode node; + try { + node = asJson(target); + } + catch (AccessException e) { + // Cannot parse - treat as not a JSON + return false; + } + if (node instanceof ArrayNode) { + return maybeIndex(name) != null; + } + return true; + } + + private JsonNode asJson(Object target) throws AccessException { + if (target instanceof JsonNode jsonNode) { + return jsonNode; + } + else if (target instanceof JsonNodeWrapper jsonNodeWrapper) { + return jsonNodeWrapper.getRealNode(); + } + else if (target instanceof String content) { + try { + return this.objectMapper.readTree(content); + } + catch (JacksonException e) { + throw new AccessException("Exception while trying to deserialize String", e); + } + } + else { + throw new IllegalStateException("Can't happen. Check SUPPORTED_CLASSES"); + } + } + + /** + * Return an integer if the String property name can be parsed as an int, or null otherwise. + */ + private static Integer maybeIndex(String name) { + if (!isNumeric(name)) { + return null; + } + try { + return Integer.valueOf(name); + } + catch (NumberFormatException e) { + return null; + } + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + JsonNode node = asJson(target); + Integer index = maybeIndex(name); + if (index != null && node.has(index)) { + return typedValue(node.get(index)); + } + else { + return typedValue(node.get(name)); + } + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) { + return false; + } + + @Override + public void write(EvaluationContext context, Object target, String name, Object newValue) { + throw new UnsupportedOperationException("Write is not supported"); + } + + /** + * Check if the string is a numeric representation (all digits) or not. + */ + private static boolean isNumeric(String str) { + if (!StringUtils.hasLength(str)) { + return false; + } + int length = str.length(); + for (int i = 0; i < length; i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } + + static TypedValue typedValue(JsonNode json) throws AccessException { + if (json == null || json instanceof NullNode) { + return TypedValue.NULL; + } + else if (json.isValueNode()) { + return new TypedValue(getValue(json)); + } + return new TypedValue(wrap(json)); + } + + private static Object getValue(JsonNode json) throws AccessException { + if (json.isString()) { + return json.asString(); + } + else if (json.isNumber()) { + return json.numberValue(); + } + else if (json.isBoolean()) { + return json.asBoolean(); + } + else if (json.isNull()) { + return null; + } + else if (json.isBinary()) { + try { + return json.binaryValue(); + } + catch (JacksonException e) { + throw new AccessException( + "Can not get content of binary value: " + json, e); + } + } + throw new IllegalArgumentException("Json is not ValueNode."); + } + + public static Object wrap(JsonNode json) throws AccessException { + if (json == null) { + return null; + } + else if (json instanceof ArrayNode arrayNode) { + return new ArrayNodeAsList(arrayNode); + } + else if (json.isValueNode()) { + return getValue(json); + } + else { + return new ComparableJsonNode(json); + } + } + + interface JsonNodeWrapper extends Comparable { + + JsonNode getRealNode(); + + } + + static class ComparableJsonNode implements JsonNodeWrapper { + + private final JsonNode delegate; + + ComparableJsonNode(JsonNode delegate) { + this.delegate = delegate; + } + + @Override + public JsonNode getRealNode() { + return this.delegate; + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + @Override + public int compareTo(ComparableJsonNode o) { + return this.delegate.equals(o.delegate) ? 0 : 1; + } + + } + + /** + * An {@link AbstractList} implementation around {@link ArrayNode} with {@link JsonNodeWrapper} aspect. + * @since 5.0 + */ + static class ArrayNodeAsList extends AbstractList implements JsonNodeWrapper { + + private final ArrayNode delegate; + + ArrayNodeAsList(ArrayNode node) { + this.delegate = node; + } + + @Override + public JsonNode getRealNode() { + return this.delegate; + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + @Override + public Object get(int index) { + // negative index - get from the end of list + int i = index < 0 ? this.delegate.size() + index : index; + try { + return wrap(this.delegate.get(i)); + } + catch (AccessException ex) { + throw new IllegalArgumentException(ex); + } + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public Iterator iterator() { + + return new Iterator<>() { + + private final Iterator it = ArrayNodeAsList.this.delegate.iterator(); + + @Override + public boolean hasNext() { + return this.it.hasNext(); + } + + @Override + public Object next() { + try { + return wrap(this.it.next()); + } + catch (AccessException e) { + throw new IllegalArgumentException(e); + } + } + + }; + } + + @Override + public int compareTo(Object o) { + Object that = (o instanceof JsonNodeWrapper wrapper ? wrapper.getRealNode() : o); + return this.delegate.equals(that) ? 0 : 1; + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonIndexAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonIndexAccessor.java index 05443044c8..dd69153226 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonIndexAccessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonIndexAccessor.java @@ -34,9 +34,12 @@ * any index that is out of bounds (see {@link ArrayNode#get(int)} for details). * * @author Sam Brannen + * * @since 6.4 * @see JsonPropertyAccessor + * @deprecated Since 7.0 in favor of {@link JacksonIndexAccessor} for Jackson 3. */ +@Deprecated(forRemoval = true, since = "7.0") public class JsonIndexAccessor implements IndexAccessor { private static final Class[] SUPPORTED_CLASSES = { ArrayNode.class }; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperConverter.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperConverter.java new file mode 100644 index 0000000000..5fc078a554 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperConverter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025-present 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.json; + +import java.util.Collections; +import java.util.Set; + +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.JsonNode; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.integration.json.JacksonPropertyAccessor.JsonNodeWrapper; + +/** + * The {@link org.springframework.core.convert.converter.Converter} implementation for the conversion + * of {@link JsonNodeWrapper} to {@link JsonNode}, + * when the {@link JsonNodeWrapper} can be a result of the expression + * for JSON in case of the {@link JacksonPropertyAccessor} usage. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class JsonNodeWrapperConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(JsonNodeWrapper.class, JsonNode.class)); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source != null) { + return targetType.getObjectType().cast(((JsonNodeWrapper) source).getRealNode()); + } + return null; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java index cbf6d7788d..6922ec6e45 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonNodeWrapperToJsonNodeConverter.java @@ -24,7 +24,6 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.integration.json.JsonPropertyAccessor.JsonNodeWrapper; /** * The {@link org.springframework.core.convert.converter.Converter} implementation for the conversion @@ -36,19 +35,22 @@ * @author Artem Bilan * * @since 5.5 + * @deprecated Since 7.0 in favor of {@link JsonNodeWrapperConverter} for Jackson 3. */ +@SuppressWarnings("removal") +@Deprecated(forRemoval = true, since = "7.0") public class JsonNodeWrapperToJsonNodeConverter implements GenericConverter { @Override public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(JsonNodeWrapper.class, JsonNode.class)); + return Collections.singleton(new ConvertiblePair(JsonPropertyAccessor.JsonNodeWrapper.class, JsonNode.class)); } @Override @Nullable public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source != null) { - return targetType.getObjectType().cast(((JsonNodeWrapper) source).getRealNode()); + return targetType.getObjectType().cast(((JsonPropertyAccessor.JsonNodeWrapper) source).getRealNode()); } return null; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java index 573420c034..ba9e78f057 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java @@ -48,7 +48,9 @@ * * @since 3.0 * @see JsonIndexAccessor + * @deprecated Since 7.0 in favor of {@link JacksonPropertyAccessor} for Jackson 3. */ +@Deprecated(forRemoval = true, since = "7.0") public class JsonPropertyAccessor implements PropertyAccessor { /** diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapper.java new file mode 100644 index 0000000000..bdf2898125 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapper.java @@ -0,0 +1,281 @@ +/* + * Copyright 2025-present 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.json; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; + +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.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; + +/** + * For outbound messages, uses a message-aware Jackson object mapper to render the message + * as JSON. For messages with {@code byte[]} payloads, if rendered as JSON, Jackson + * performs Base64 conversion on the bytes. If payload is {@code byte[]} and + * the {@link #setRawBytes(boolean) rawBytes} property is true (default), the result has the form + * {@code [headersLen][headers][payloadLen][payload]}; with the headers + * rendered in JSON and the payload unchanged. + * Otherwise, message is fully serialized and deserialized with Jackson object mapper. + *

+ * By default, all headers are included; you can provide simple patterns to specify a + * subset of headers. + *

+ * If neither expected format is detected, nor an error occurs during conversion, the + * payload of the message is the original {@code byte[]}. + *

+ * IMPORTANT + *

+ * The default object mapper will only deserialize classes in certain packages. + * + *

+ *	"java.util",
+ *	"java.lang",
+ *	"org.springframework.messaging.support",
+ *	"org.springframework.integration.support",
+ *	"org.springframework.integration.message",
+ *	"org.springframework.integration.store",
+ *	"org.springframework.integration.history",
+ * 	"org.springframework.integration.handler"
+ * 
+ *

+ * To add more packages, create an object mapper using + * {@link JacksonMessagingUtils#messagingAwareMapper(String...)}. + *

+ * A constructor is provided allowing the provision of such a configured object mapper. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class EmbeddedHeadersJsonMessageMapper implements BytesMessageMapper { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final ObjectMapper objectMapper; + + private final String[] headerPatterns; + + private final boolean allHeaders; + + private boolean rawBytes = true; + + private boolean caseSensitive; + + /** + * Construct an instance that embeds all headers, using the default + * JSON Object mapper. + */ + public EmbeddedHeadersJsonMessageMapper() { + this("*"); + } + + /** + * Construct an instance that embeds headers matching the supplied patterns, using + * the default JSON object mapper. + * @param headerPatterns the patterns. + * @see PatternMatchUtils#smartMatch(String, String...) + */ + public EmbeddedHeadersJsonMessageMapper(String... headerPatterns) { + this(JacksonMessagingUtils.messagingAwareMapper(), headerPatterns); + } + + /** + * Construct an instance that embeds all headers, using the + * supplied JSON object mapper. + * @param objectMapper the object mapper. + */ + public EmbeddedHeadersJsonMessageMapper(ObjectMapper objectMapper) { + this(objectMapper, "*"); + } + + /** + * Construct an instance that embeds headers matching the supplied patterns using the + * supplied JSON object mapper. + * @param objectMapper the object mapper. + * @param headerPatterns the patterns. + */ + public EmbeddedHeadersJsonMessageMapper(ObjectMapper objectMapper, String... headerPatterns) { + this.objectMapper = objectMapper; + this.headerPatterns = Arrays.copyOf(headerPatterns, headerPatterns.length); + this.allHeaders = this.headerPatterns.length == 1 && this.headerPatterns[0].equals("*"); + } + + /** + * For messages with {@code byte[]} payloads, if rendered as JSON, Jackson performs + * Base64 conversion on the bytes. If this property is true (default), the result has + * the form {@code [headersLen][headers][payloadLen][payload]}; with + * the headers rendered in JSON and the payload unchanged. + * Set to {@code false} to render the bytes as base64. + * @param rawBytes false to encode as base64. + */ + public void setRawBytes(boolean rawBytes) { + this.rawBytes = rawBytes; + } + + /** + * Set to true to make the header name pattern match case-sensitive. + * Default false. + * @param caseSensitive true to make case-sensitive. + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public Collection getHeaderPatterns() { + return Arrays.asList(this.headerPatterns); + } + + @Override + public byte[] fromMessage(Message message) { + Map headersToEncode = + this.allHeaders + ? message.getHeaders() + : pruneHeaders(message.getHeaders()); + + if (this.rawBytes && message.getPayload() instanceof byte[] bytes) { + return fromBytesPayload(bytes, 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); + } + + try { + return this.objectMapper.writeValueAsBytes(messageToEncode); + } + catch (JacksonException ex) { + throw new UncheckedIOException(new IOException(ex)); + } + } + } + + private Map pruneHeaders(MessageHeaders messageHeaders) { + return messageHeaders + .entrySet() + .stream() + .filter(e -> matchHeader(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + 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 headersToEncode) { + try { + byte[] headers = this.objectMapper.writeValueAsBytes(headersToEncode); + ByteBuffer buffer = ByteBuffer.wrap(new byte[8 + headers.length + payload.length]); + buffer.putInt(headers.length); + buffer.put(headers); + buffer.putInt(payload.length); + buffer.put(payload); + return buffer.array(); + } + catch (JacksonException ex) { + throw new UncheckedIOException(new IOException(ex)); + } + } + + @Override + public Message toMessage(byte[] bytes, @Nullable Map headers) { + Message message = null; + try { + message = decodeNativeFormat(bytes, headers); + } + catch (@SuppressWarnings("unused") Exception ex) { + this.logger.debug("Failed to decode native format", ex); + } + if (message == null) { + try { + message = (Message) this.objectMapper.readValue(bytes, Object.class); + } + catch (Exception ex) { + this.logger.debug("Failed to decode JSON", ex); + } + } + if (message != null) { + return message; + } + else { + return headers == null ? new GenericMessage<>(bytes) : new GenericMessage<>(bytes, headers); + } + } + + @Nullable + private Message decodeNativeFormat(byte[] bytes, @Nullable Map headersToAdd) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + if (buffer.remaining() > 4) { + int headersLen = buffer.getInt(); + if (headersLen >= 0 && headersLen < buffer.remaining() - 4) { + ((Buffer) buffer).position(headersLen + 4); + int payloadLen = buffer.getInt(); + if (payloadLen != buffer.remaining()) { + return null; + } + else { + ((Buffer) buffer).position(4); + @SuppressWarnings("unchecked") + Map headers = this.objectMapper.readValue(bytes, buffer.position(), headersLen, + Map.class); + + ((Buffer) buffer).position(buffer.position() + headersLen); + buffer.getInt(); + Object payload; + byte[] payloadBytes = new byte[payloadLen]; + buffer.get(payloadBytes); + payload = payloadBytes; + + if (headersToAdd != null) { + headersToAdd.forEach(headers::putIfAbsent); + } + + return new GenericMessage<>(payload, new MutableMessageHeaders(headers)); + } + } + } + return null; + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJsonAccessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJacksonAccessorTests.java similarity index 82% rename from spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJsonAccessorTests.java rename to spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJacksonAccessorTests.java index 12078e41a5..2183277c1e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJsonAccessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/AbstractJacksonAccessorTests.java @@ -18,14 +18,14 @@ import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.node.StringNode; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.Expression; @@ -34,24 +34,25 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; -import org.springframework.integration.json.JsonPropertyAccessor.ArrayNodeAsList; -import org.springframework.integration.json.JsonPropertyAccessor.ComparableJsonNode; +import org.springframework.integration.json.JacksonPropertyAccessor.ArrayNodeAsList; +import org.springframework.integration.json.JacksonPropertyAccessor.ComparableJsonNode; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Abstract base class for tests involving {@link JsonPropertyAccessor} and {@link JsonIndexAccessor}. + * Abstract base class for tests involving {@link JacksonPropertyAccessor} and {@link JacksonIndexAccessor}. * * @author Eric Bottard * @author Artem Bilan * @author Paul Martin * @author Pierre Lakreb * @author Sam Brannen + * @author Jooyoung Pyoung * * @since 3.0 */ -abstract class AbstractJsonAccessorTests { +public abstract class AbstractJacksonAccessorTests { protected final SpelExpressionParser parser = new SpelExpressionParser(); @@ -62,7 +63,7 @@ abstract class AbstractJsonAccessorTests { @BeforeEach void setUpEvaluationContext() { DefaultConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter(new JsonNodeWrapperToJsonNodeConverter()); + conversionService.addConverter(new JsonNodeWrapperConverter()); context.setTypeConverter(new StandardTypeConverter(conversionService)); } @@ -74,26 +75,26 @@ void setUpEvaluationContext() { class JsonNodeTests { @Test - void textNode() throws Exception { - TextNode json = (TextNode) mapper.readTree("\"foo\""); + void textNode() { + StringNode json = (StringNode) mapper.readTree("\"foo\""); String result = evaluate(json, "#root", String.class); assertThat(result).isEqualTo("\"foo\""); } @Test - void nullProperty() throws Exception { + void nullProperty() { JsonNode json = mapper.readTree("{\"foo\": null}"); assertThat(evaluate(json, "foo", String.class)).isNull(); } @Test - void missingProperty() throws Exception { + void missingProperty() { JsonNode json = mapper.readTree(FOO_BAR_JSON); assertThat(evaluate(json, "fizz", String.class)).isNull(); } @Test - void propertyLookup() throws Exception { + void propertyLookup() { JsonNode json1 = mapper.readTree(FOO_BAR_JSON); String value1 = evaluate(json1, "foo", String.class); assertThat(value1).isEqualTo("bar"); @@ -106,7 +107,7 @@ void propertyLookup() throws Exception { void arrayLookupWithIntegerIndexAndExplicitWrapping() throws Exception { ArrayNode json = (ArrayNode) mapper.readTree("[3, 4, 5]"); // Have to wrap the root array because ArrayNode itself is not a List - Integer actual = evaluate(JsonPropertyAccessor.wrap(json), "[1]", Integer.class); + Integer actual = evaluate(JacksonPropertyAccessor.wrap(json), "[1]", Integer.class); assertThat(actual).isEqualTo(4); } @@ -114,19 +115,19 @@ void arrayLookupWithIntegerIndexAndExplicitWrapping() throws Exception { void arrayLookupWithIntegerIndexForNullValueAndExplicitWrapping() throws Exception { ArrayNode json = (ArrayNode) mapper.readTree("[3, null, 5]"); // Have to wrap the root array because ArrayNode itself is not a List - Integer actual = evaluate(JsonPropertyAccessor.wrap(json), "[1]", Integer.class); + Integer actual = evaluate(JacksonPropertyAccessor.wrap(json), "[1]", Integer.class); assertThat(actual).isNull(); } @Test - void arrayLookupWithNegativeIntegerIndex() throws Exception { + void arrayLookupWithNegativeIntegerIndex() { JsonNode json = mapper.readTree("{\"foo\": [3, 4, 5]}"); // ArrayNodeAsList allows one to index into a JSON array via a negative index. assertThat(evaluate(json, "foo[-1]", Integer.class)).isEqualTo(5); } @Test - void arrayLookupWithNegativeIntegerIndexGreaterThanArrayLength() throws Exception { + void arrayLookupWithNegativeIntegerIndexGreaterThanArrayLength() { JsonNode json = mapper.readTree("{\"foo\": [3, 4, 5]}"); // Although ArrayNodeAsList allows one to index into a JSON array via a negative // index, if the result of (array.length - index) is still negative, Jackson's @@ -135,14 +136,14 @@ void arrayLookupWithNegativeIntegerIndexGreaterThanArrayLength() throws Exceptio } @Test - void arrayLookupWithNegativeIntegerIndexForNullValue() throws Exception { + void arrayLookupWithNegativeIntegerIndexForNullValue() { JsonNode json = mapper.readTree("{\"foo\": [3, 4, null]}"); // ArrayNodeAsList allows one to index into a JSON array via a negative index. assertThat(evaluate(json, "foo[-1]", Integer.class)).isNull(); } @Test - void arrayLookupWithIntegerIndexOutOfBounds() throws Exception { + void arrayLookupWithIntegerIndexOutOfBounds() { JsonNode json = mapper.readTree("{\"foo\": [3, 4, 5]}"); assertThatExceptionOfType(SpelEvaluationException.class) .isThrownBy(() -> evaluate(json, "foo[3]", Object.class)) @@ -150,7 +151,7 @@ void arrayLookupWithIntegerIndexOutOfBounds() throws Exception { } @Test - void arrayLookupWithStringIndex() throws Exception { + void arrayLookupWithStringIndex() { JsonNode json = mapper.readTree("[3, 4, 5]"); Integer actual = evaluate(json, "['1']", Integer.class); assertThat(actual).isEqualTo(4); @@ -159,13 +160,12 @@ void arrayLookupWithStringIndex() throws Exception { @Test void nestedArrayLookupWithIntegerIndexAndExplicitWrapping() throws Exception { ArrayNode json = (ArrayNode) mapper.readTree("[[3], [4, 5], []]"); - // JsonNode actual = evaluate(json, "1.1", JsonNode.class); // Does not work - Object actual = evaluate(JsonPropertyAccessor.wrap(json), "[1][1]", Object.class); + Object actual = evaluate(JacksonPropertyAccessor.wrap(json), "[1][1]", Object.class); assertThat(actual).isEqualTo(5); } @Test - void nestedArrayLookupWithStringIndex() throws Exception { + void nestedArrayLookupWithStringIndex() { JsonNode json = mapper.readTree("[[3], [4, 5], []]"); Integer actual = evaluate(json, "['1']['1']", Integer.class); assertThat(actual).isEqualTo(5); @@ -173,7 +173,7 @@ void nestedArrayLookupWithStringIndex() throws Exception { @Test @SuppressWarnings("unchecked") - void nestedArrayLookupWithStringIndexAndThenIntegerIndex() throws Exception { + void nestedArrayLookupWithStringIndexAndThenIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[[3], [4, 5], []]"); List list = evaluate(arrayNode, "['0']", List.class); @@ -188,7 +188,7 @@ void nestedArrayLookupWithStringIndexAndThenIntegerIndex() throws Exception { } @Test - void arrayProjection() throws Exception { + void arrayProjection() { JsonNode json = mapper.readTree(FOO_BAR_ARRAY_FIZZ_JSON); // Filter the bar array to return only the fizz value of each element (to prove that SpEL considers bar @@ -201,7 +201,7 @@ void arrayProjection() throws Exception { } @Test - void arraySelection() throws Exception { + void arraySelection() { JsonNode json = mapper.readTree(FOO_BAR_ARRAY_FIZZ_JSON); // Filter bar objects so that none match @@ -221,7 +221,7 @@ void arraySelection() throws Exception { } @Test - void nestedPropertyAccessViaJsonNode() throws Exception { + void nestedPropertyAccessViaJsonNode() { JsonNode json = mapper.readTree(FOO_BAR_FIZZ_JSON); assertThat(evaluate(json, "foo.bar", Integer.class)).isEqualTo(4); @@ -229,7 +229,7 @@ void nestedPropertyAccessViaJsonNode() throws Exception { } @Test - void noNullPointerExceptionWithCachedReadAccessor() throws Exception { + void noNullPointerExceptionWithCachedReadAccessor() { Expression expression = parser.parseExpression("foo"); JsonNode json1 = mapper.readTree(FOO_BAR_JSON); String value1 = expression.getValue(context, json1, String.class); @@ -255,7 +255,7 @@ void selectorAccess() { } @Test - void nestedPropertyAccessViaJsonAsString() throws Exception { + void nestedPropertyAccessViaJsonAsString() { String json = FOO_BAR_FIZZ_JSON; assertThat(evaluate(json, "foo.bar", Integer.class)).isEqualTo(4); @@ -263,34 +263,34 @@ void nestedPropertyAccessViaJsonAsString() throws Exception { } @Test - void jsonGetValueConversionAsJsonNode() throws Exception { + void jsonGetValueConversionAsJsonNode() { // use JsonNode conversion JsonNode node = evaluate(PROPERTY_NAMES_JSON, "property.^[name == 'value1']", JsonNode.class); assertThat(node).isEqualTo(mapper.readTree("{\"name\":\"value1\"}")); } @Test - void jsonGetValueConversionAsObjectNode() throws Exception { + void jsonGetValueConversionAsObjectNode() { // use ObjectNode conversion ObjectNode node = evaluate(PROPERTY_NAMES_JSON, "property.^[name == 'value1']", ObjectNode.class); assertThat(node).isEqualTo(mapper.readTree("{\"name\":\"value1\"}")); } @Test - void jsonGetValueConversionAsArrayNode() throws Exception { + void jsonGetValueConversionAsArrayNode() { // use ArrayNode conversion ArrayNode node = evaluate(PROPERTY_NAMES_JSON, "property", ArrayNode.class); assertThat(node).isEqualTo(mapper.readTree("[{\"name\":\"value1\"},{\"name\":\"value2\"}]")); } @Test - void comparingArrayNode() throws Exception { + void comparingArrayNode() { Boolean actual = evaluate(PROPERTIES_WITH_NAMES_JSON, "property1 eq property2", Boolean.class); assertThat(actual).isTrue(); } @Test - void comparingJsonNode() throws Exception { + void comparingJsonNode() { Boolean actual = evaluate(PROPERTIES_WITH_NAMES_JSON, "property1[0] eq property2[0]", Boolean.class); assertThat(actual).isTrue(); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonIndexAccessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JacksonIndexAccessorTests.java similarity index 70% rename from spring-integration-core/src/test/java/org/springframework/integration/json/JsonIndexAccessorTests.java rename to spring-integration-core/src/test/java/org/springframework/integration/json/JacksonIndexAccessorTests.java index c8e64fd96e..2f43114a4c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonIndexAccessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JacksonIndexAccessorTests.java @@ -18,73 +18,76 @@ import java.util.List; -import com.fasterxml.jackson.databind.node.ArrayNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ArrayNode; -import org.springframework.integration.json.JsonPropertyAccessor.ArrayNodeAsList; +import org.springframework.integration.json.JacksonPropertyAccessor.ArrayNodeAsList; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link JsonIndexAccessor} combined with {@link JsonPropertyAccessor}. + * Tests for {@link JacksonIndexAccessor} combined with {@link JacksonPropertyAccessor}. * * @author Sam Brannen + * @author Jooyoung Pyoung + * * @since 6.4 - * @see JsonPropertyAccessorTests + * + * @see JacksonPropertyAccessorTests */ -class JsonIndexAccessorTests extends AbstractJsonAccessorTests { +class JacksonIndexAccessorTests extends AbstractJacksonAccessorTests { @BeforeEach void registerJsonAccessors() { - context.addIndexAccessor(new JsonIndexAccessor()); - // We also register a JsonPropertyAccessor to ensure that the JsonIndexAccessor - // does not interfere with the feature set of the JsonPropertyAccessor. - context.addPropertyAccessor(new JsonPropertyAccessor()); + context.addIndexAccessor(new JacksonIndexAccessor()); + // We also register a JacksonPropertyAccessor to ensure that the JacksonIndexAccessor + // does not interfere with the feature set of the JacksonPropertyAccessor. + context.addPropertyAccessor(new JacksonPropertyAccessor()); } /** * Tests which index directly into a Jackson {@link ArrayNode}, which is only supported - * by {@link JsonIndexAccessor}. + * by {@link JacksonPropertyAccessor}. */ @Nested class ArrayNodeTests { @Test - void indexDirectlyIntoArrayNodeWithIntegerIndex() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); Integer actual = evaluate(arrayNode, "[1]", Integer.class); assertThat(actual).isEqualTo(4); } @Test - void indexDirectlyIntoArrayNodeWithIntegerIndexForNullValue() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndexForNullValue() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, null, 5]"); Integer actual = evaluate(arrayNode, "[1]", Integer.class); assertThat(actual).isNull(); } @Test - void indexDirectlyIntoArrayNodeWithNegativeIntegerIndex() throws Exception { + void indexDirectlyIntoArrayNodeWithNegativeIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); Integer actual = evaluate(arrayNode, "[-1]", Integer.class); - // JsonIndexAccessor allows one to index into a JSON array via a negative index. + // JacksonIndexAccessor allows one to index into a JSON array via a negative index. assertThat(actual).isEqualTo(5); } @Test - void indexDirectlyIntoArrayNodeWithNegativeIntegerIndexGreaterThanArrayLength() throws Exception { + void indexDirectlyIntoArrayNodeWithNegativeIntegerIndexGreaterThanArrayLength() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); Integer actual = evaluate(arrayNode, "[-99]", Integer.class); - // Although JsonIndexAccessor allows one to index into a JSON array via a negative + // Although JacksonIndexAccessor allows one to index into a JSON array via a negative // index, if the result of (array.length - index) is still negative, Jackson's // ArrayNode.get() method returns null instead of throwing an IndexOutOfBoundsException. assertThat(actual).isNull(); } @Test - void indexDirectlyIntoArrayNodeWithIntegerIndexOutOfBounds() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndexOutOfBounds() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); Integer actual = evaluate(arrayNode, "[9999]", Integer.class); // Jackson's ArrayNode.get() method always returns null instead of throwing an IndexOutOfBoundsException. @@ -92,11 +95,11 @@ void indexDirectlyIntoArrayNodeWithIntegerIndexOutOfBounds() throws Exception { } /** - * @see AbstractJsonAccessorTests.JsonNodeTests#nestedArrayLookupWithStringIndexAndThenIntegerIndex() + * @see JsonNodeTests#nestedArrayLookupWithStringIndexAndThenIntegerIndex() */ @Test @SuppressWarnings("unchecked") - void nestedArrayLookupsWithIntegerIndexes() throws Exception { + void nestedArrayLookupsWithIntegerIndexes() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[[3], [4, 5], []]"); List list = evaluate(arrayNode, "[0]", List.class); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JacksonPropertyAccessorTests.java similarity index 77% rename from spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java rename to spring-integration-core/src/test/java/org/springframework/integration/json/JacksonPropertyAccessorTests.java index 3644353d11..6bbfedef6f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPropertyAccessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JacksonPropertyAccessorTests.java @@ -16,10 +16,10 @@ package org.springframework.integration.json; -import com.fasterxml.jackson.databind.node.ArrayNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import tools.jackson.databind.node.ArrayNode; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; @@ -28,60 +28,62 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Tests for {@link JsonPropertyAccessor}. + * Tests for {@link JacksonPropertyAccessor}. * * @author Eric Bottard * @author Artem Bilan * @author Paul Martin * @author Pierre Lakreb * @author Sam Brannen + * @author Jooyoung Pyoung * * @since 3.0 - * @see JsonIndexAccessorTests + * + * @see JacksonIndexAccessorTests */ -class JsonPropertyAccessorTests extends AbstractJsonAccessorTests { +public class JacksonPropertyAccessorTests extends AbstractJacksonAccessorTests { @BeforeEach void registerJsonPropertyAccessor() { - context.addPropertyAccessor(new JsonPropertyAccessor()); + context.addPropertyAccessor(new JacksonPropertyAccessor()); } /** * Tests which index directly into a Jackson {@link ArrayNode}, which is not supported - * by {@link JsonPropertyAccessor}. + * by {@link JacksonPropertyAccessor}. */ @Nested class ArrayNodeTests { @Test - void indexDirectlyIntoArrayNodeWithIntegerIndex() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); assertIndexingNotSupported(arrayNode, "[1]"); } @Test - void indexDirectlyIntoArrayNodeWithIntegerIndexForNullValue() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndexForNullValue() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, null, 5]"); assertIndexingNotSupported(arrayNode, "[1]"); } @Test - void indexDirectlyIntoArrayNodeWithNegativeIntegerIndex() throws Exception { + void indexDirectlyIntoArrayNodeWithNegativeIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); assertIndexingNotSupported(arrayNode, "[-1]"); } @Test - void indexDirectlyIntoArrayNodeWithIntegerIndexOutOfBounds() throws Exception { + void indexDirectlyIntoArrayNodeWithIntegerIndexOutOfBounds() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[3, 4, 5]"); assertIndexingNotSupported(arrayNode, "[9999]"); } /** - * @see AbstractJsonAccessorTests.JsonNodeTests#nestedArrayLookupWithStringIndexAndThenIntegerIndex() + * @see JsonNodeTests#nestedArrayLookupWithStringIndexAndThenIntegerIndex() */ @Test - void nestedArrayLookupWithIntegerIndexAndThenIntegerIndex() throws Exception { + void nestedArrayLookupWithIntegerIndexAndThenIntegerIndex() { ArrayNode arrayNode = (ArrayNode) mapper.readTree("[[3], [4, 5], []]"); assertIndexingNotSupported(arrayNode, "[1][1]"); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapperTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapperTests.java new file mode 100644 index 0000000000..b882323bd9 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/json/EmbeddedHeadersJsonMessageMapperTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2025-present 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.json; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class EmbeddedHeadersJsonMessageMapperTests { + + @Test + public void testEmbedAll() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper(); + GenericMessage message = new GenericMessage<>("foo"); + assertThat(mapper.toMessage(mapper.fromMessage(message))).isEqualTo(message); + } + + @Test + public void testEmbedSome() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper(MessageHeaders.ID); + GenericMessage message = new GenericMessage<>("foo"); + 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 encodedMessageToCheck = + objectMapper.readValue(encodedMessage, new TypeReference<>() { + + }); + + Object headers = encodedMessageToCheck.get("headers"); + assertThat(headers).isNotNull(); + assertThat(headers).isInstanceOf(Map.class); + + @SuppressWarnings("unchecked") + Map headersToCheck = (Map) headers; + assertThat(headersToCheck).doesNotContainKey(MessageHeaders.TIMESTAMP); + } + + @Test + public void testBytesEmbedAll() throws Exception { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper(); + GenericMessage message = new GenericMessage<>("foo".getBytes()); + Thread.sleep(2); + byte[] bytes = mapper.fromMessage(message); + ByteBuffer bb = ByteBuffer.wrap(bytes); + int headerLen = bb.getInt(); + byte[] headerBytes = new byte[headerLen]; + bb.get(headerBytes); + String headers = new String(headerBytes); + 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 + public void testBytesEmbedSome() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper("I*"); + GenericMessage message = new GenericMessage<>("foo".getBytes(), Collections.singletonMap("bar", "baz")); + byte[] bytes = mapper.fromMessage(message); + ByteBuffer bb = ByteBuffer.wrap(bytes); + int headerLen = bb.getInt(); + byte[] headerBytes = new byte[headerLen]; + bb.get(headerBytes); + String headers = new String(headerBytes); + 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 + public void testBytesEmbedAllJson() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper(); + mapper.setRawBytes(false); + GenericMessage message = new GenericMessage<>("foo".getBytes()); + byte[] mappedBytes = mapper.fromMessage(message); + String mapped = new String(mappedBytes); + assertThat(mapped).contains("[B\",\"Zm9v"); + @SuppressWarnings("unchecked") + Message decoded = (Message) mapper.toMessage(mappedBytes); + assertThat(new String(decoded.getPayload())).isEqualTo("foo"); + + } + + @Test + public void testBytesDecodeAll() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper(); + GenericMessage message = new GenericMessage<>("foo".getBytes()); + Message decoded = mapper.toMessage(mapper.fromMessage(message)); + assertThat(decoded).isEqualTo(message); + } + + @Test + public void testDontMapIdButOthers() { + EmbeddedHeadersJsonMessageMapper mapper = new EmbeddedHeadersJsonMessageMapper("!" + MessageHeaders.ID, "*"); + GenericMessage message = new GenericMessage<>("foo", Collections.singletonMap("bar", "baz")); + byte[] encodedMessage = mapper.fromMessage(message); + + ObjectMapper objectMapper = new ObjectMapper(); + Map encodedMessageToCheck = + objectMapper.readValue(encodedMessage, new TypeReference<>() { + + }); + + Object headers = encodedMessageToCheck.get("headers"); + assertThat(headers).isNotNull(); + assertThat(headers).isInstanceOf(Map.class); + + @SuppressWarnings("unchecked") + Map headersToCheck = (Map) 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"); + } + +}