diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/AdviceMessageJsonDeserializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AdviceMessageJsonDeserializer.java new file mode 100644 index 0000000000..4116464a6c --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AdviceMessageJsonDeserializer.java @@ -0,0 +1,49 @@ +/* + * 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 tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; + +import org.springframework.integration.message.AdviceMessage; +import org.springframework.integration.support.MutableMessageHeaders; +import org.springframework.messaging.Message; + +/** + * The {@link MessageJsonDeserializer} implementation for the {@link AdviceMessage}. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class AdviceMessageJsonDeserializer extends MessageJsonDeserializer> { + + @SuppressWarnings("unchecked") + public AdviceMessageJsonDeserializer() { + super((Class>) (Class) AdviceMessage.class); + } + + @Override + protected AdviceMessage buildMessage(MutableMessageHeaders headers, Object payload, JsonNode root, + DeserializationContext ctxt) throws JacksonException { + + Message inputMessage = getMapper().readValue(root.get("inputMessage").traverse(ctxt), Message.class); + return new AdviceMessage<>(payload, headers, inputMessage); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/ErrorMessageJsonDeserializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/ErrorMessageJsonDeserializer.java new file mode 100644 index 0000000000..9f7874f98e --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/ErrorMessageJsonDeserializer.java @@ -0,0 +1,51 @@ +/* + * 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 tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.type.TypeFactory; + +import org.springframework.integration.support.MutableMessageHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.ErrorMessage; + +/** + * The {@link MessageJsonDeserializer} implementation for the {@link ErrorMessage}. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class ErrorMessageJsonDeserializer extends MessageJsonDeserializer { + + @SuppressWarnings("this-escape") + public ErrorMessageJsonDeserializer() { + super(ErrorMessage.class); + setPayloadType(TypeFactory.createDefaultInstance().constructType(Throwable.class)); + } + + @Override + protected ErrorMessage buildMessage(MutableMessageHeaders headers, Object payload, JsonNode root, + DeserializationContext ctxt) throws JacksonException { + + Message originalMessage = getMapper().readValue(root.get("originalMessage").traverse(ctxt), Message.class); + return new ErrorMessage((Throwable) payload, headers, originalMessage); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/GenericMessageJsonDeserializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/GenericMessageJsonDeserializer.java new file mode 100644 index 0000000000..8476b87f6c --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/GenericMessageJsonDeserializer.java @@ -0,0 +1,47 @@ +/* + * 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 tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; + +import org.springframework.integration.support.MutableMessageHeaders; +import org.springframework.messaging.support.GenericMessage; + +/** + * The {@link MessageJsonDeserializer} implementation for the {@link GenericMessage}. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class GenericMessageJsonDeserializer extends MessageJsonDeserializer> { + + @SuppressWarnings("unchecked") + public GenericMessageJsonDeserializer() { + super((Class>) (Class) GenericMessage.class); + } + + @Override + protected GenericMessage buildMessage(MutableMessageHeaders headers, Object payload, JsonNode root, + DeserializationContext ctxt) throws JacksonException { + + return new GenericMessage<>(payload, headers); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonMessagingUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonMessagingUtils.java new file mode 100644 index 0000000000..56003d1694 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonMessagingUtils.java @@ -0,0 +1,251 @@ +/* + * 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.Serial; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.NamedType; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.impl.DefaultTypeResolverBuilder; +import tools.jackson.databind.module.SimpleModule; + +import org.springframework.integration.message.AdviceMessage; +import org.springframework.integration.support.MutableMessage; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; + +/** + * Utility for creating Jackson {@link ObjectMapper} instance for Spring messaging. + * + *

Provides custom serializers/deserializers for Spring messaging types + * and validates deserialization against trusted package patterns. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public final class JacksonMessagingUtils { + + /** + * The packages to trust on JSON deserialization by default. + */ + public static final List DEFAULT_TRUSTED_PACKAGES = + List.of( + "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" + ); + + private JacksonMessagingUtils() { + } + + /** + * Return an {@link ObjectMapper} if available, + * supplied with Message specific serializers and deserializers. + * Also configured to store typo info in the {@code @class} property. + * @param trustedPackages the trusted Java packages for deserialization. + * @return the mapper. + * @throws IllegalStateException if an implementation is not available. + * @since 7.0 + */ + public static ObjectMapper messagingAwareMapper(String @Nullable ... trustedPackages) { + if (JacksonPresent.isJackson3Present()) { + GenericMessageJsonDeserializer genericMessageDeserializer = new GenericMessageJsonDeserializer(); + ErrorMessageJsonDeserializer errorMessageDeserializer = new ErrorMessageJsonDeserializer(); + AdviceMessageJsonDeserializer adviceMessageDeserializer = new AdviceMessageJsonDeserializer(); + MutableMessageJsonDeserializer mutableMessageDeserializer = new MutableMessageJsonDeserializer(); + + SimpleModule simpleModule = new SimpleModule() + .addSerializer(new MessageHeadersJsonSerializer()) + .addSerializer(new MimeTypeJsonSerializer()) + .addDeserializer(GenericMessage.class, genericMessageDeserializer) + .addDeserializer(ErrorMessage.class, errorMessageDeserializer) + .addDeserializer(AdviceMessage.class, adviceMessageDeserializer) + .addDeserializer(MutableMessage.class, mutableMessageDeserializer); + + ObjectMapper mapper = JsonMapper.builder() + .findAndAddModules(JacksonMessagingUtils.class.getClassLoader()) + .setDefaultTyping(new AllowListTypeResolverBuilder(trustedPackages)) + .addModules(simpleModule) + .build(); + + genericMessageDeserializer.setMapper(mapper); + errorMessageDeserializer.setMapper(mapper); + adviceMessageDeserializer.setMapper(mapper); + mutableMessageDeserializer.setMapper(mapper); + + return mapper; + } + else { + throw new IllegalStateException("No jackson-databind.jar is present in the classpath."); + } + } + + /** + * An implementation of {@link DefaultTypeResolverBuilder} + * that wraps a default {@link TypeIdResolver} to the {@link AllowListTypeIdResolver}. + * + * @author Jooyoung Pyoung + */ + private static final class AllowListTypeResolverBuilder extends DefaultTypeResolverBuilder { + + @Serial + private static final long serialVersionUID = 1L; + + private final String @Nullable [] trustedPackages; + + AllowListTypeResolverBuilder(String @Nullable ... trustedPackages) { + super( + BasicPolymorphicTypeValidator.builder().allowIfSubType(Object.class).build(), + DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY + ); + + this.trustedPackages = + trustedPackages != null ? Arrays.copyOf(trustedPackages, trustedPackages.length) : null; + } + + @Override + protected TypeIdResolver idResolver(DatabindContext ctxt, + JavaType baseType, PolymorphicTypeValidator subtypeValidator, + Collection subtypes, boolean forSer, boolean forDeser) { + + TypeIdResolver result = super.idResolver(ctxt, baseType, subtypeValidator, subtypes, forSer, forDeser); + return new AllowListTypeIdResolver(result, this.trustedPackages); + } + + } + + /** + * A {@link TypeIdResolver} that delegates to an existing implementation + * and throws an IllegalStateException if the class being looked up is not trusted, + * does not provide an explicit mixin mappings. + * + * @author Jooyoung Pyoung + */ + private static final class AllowListTypeIdResolver implements TypeIdResolver { + + private final TypeIdResolver delegate; + + private final Set trustedPackages = new LinkedHashSet<>(DEFAULT_TRUSTED_PACKAGES); + + AllowListTypeIdResolver(TypeIdResolver delegate, String @Nullable ... trustedPackages) { + this.delegate = delegate; + if (trustedPackages != null) { + for (String trustedPackage : trustedPackages) { + if ("*".equals(trustedPackage)) { + this.trustedPackages.clear(); + break; + } + else { + this.trustedPackages.add(trustedPackage); + } + } + } + } + + @Override + public void init(JavaType baseType) throws JacksonException { + this.delegate.init(baseType); + } + + @Override + public String idFromValue(DatabindContext ctxt, Object value) throws JacksonException { + return this.delegate.idFromValue(ctxt, value); + } + + @Override + public String idFromValueAndType(DatabindContext ctxt, Object value, Class suggestedType) throws JacksonException { + return this.delegate.idFromValueAndType(ctxt, value, suggestedType); + } + + @Override + public String idFromBaseType(DatabindContext ctxt) throws JacksonException { + return this.delegate.idFromBaseType(ctxt); + } + + @Override + public JavaType typeFromId(DatabindContext ctxt, String id) throws JacksonException { + JavaType result = this.delegate.typeFromId(ctxt, id); + + Package aPackage = result.getRawClass().getPackage(); + if (aPackage == null || isTrustedPackage(aPackage.getName())) { + return result; + } + + MapperConfig config = ctxt.getConfig(); + boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null; + if (isExplicitMixin) { + return result; + } + + throw new IllegalArgumentException("The class with " + id + " and name of " + + "" + result.getRawClass().getName() + " is not in the trusted packages: " + + "" + this.trustedPackages + ". " + + "If you believe this class is safe to deserialize, please provide its name or an explicit Mixin. " + + "If the serialization is only done by a trusted source, you can also enable default typing."); + } + + private boolean isTrustedPackage(String packageName) { + if (!this.trustedPackages.isEmpty()) { + for (String trustedPackage : this.trustedPackages) { + if (packageName.equals(trustedPackage) || + (!packageName.equals("java.util.logging") + && packageName.startsWith(trustedPackage + "."))) { + + return true; + } + } + return false; + } + + return true; + } + + @Override + public String getDescForKnownTypeIds() { + return this.delegate.getDescForKnownTypeIds(); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return this.delegate.getMechanism(); + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageHeadersJsonSerializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageHeadersJsonSerializer.java new file mode 100644 index 0000000000..81e511b98c --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageHeadersJsonSerializer.java @@ -0,0 +1,58 @@ +/* + * 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.util.HashMap; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.ser.std.StdSerializer; + +import org.springframework.messaging.MessageHeaders; + +/** + * A Jackson {@link StdSerializer} implementation to serialize {@link MessageHeaders} + * as a {@link HashMap}. + *

+ * This technique is much reliable during deserialization, especially when the + * {@code typeId} property is used to store the type. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class MessageHeadersJsonSerializer extends StdSerializer { + + public MessageHeadersJsonSerializer() { + super(MessageHeaders.class); + } + + @Override + public void serializeWithType(MessageHeaders value, JsonGenerator gen, SerializationContext ctxt, + TypeSerializer typeSer) throws JacksonException { + + serialize(value, gen, ctxt); + } + + @Override + public void serialize(MessageHeaders value, JsonGenerator gen, SerializationContext provider) throws JacksonException { + gen.writePOJO(new HashMap<>(value)); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageJsonDeserializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageJsonDeserializer.java new file mode 100644 index 0000000000..a270bcffac --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MessageJsonDeserializer.java @@ -0,0 +1,87 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.deser.std.StdNodeBasedDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.type.TypeFactory; + +import org.springframework.integration.support.MutableMessageHeaders; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * A Jackson {@link StdNodeBasedDeserializer} extension for {@link Message} implementations. + * + * @param the message type. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public abstract class MessageJsonDeserializer> extends StdNodeBasedDeserializer { + + private JavaType payloadType = TypeFactory.createDefaultInstance().constructType(Object.class); + + private ObjectMapper mapper = new ObjectMapper(); + + protected MessageJsonDeserializer(Class targetType) { + super(targetType); + } + + public void setMapper(ObjectMapper mapper) { + Assert.notNull(mapper, "'mapper' must not be null"); + this.mapper = mapper; + } + + protected final void setPayloadType(JavaType payloadType) { + Assert.notNull(payloadType, "'payloadType' must not be null"); + this.payloadType = payloadType; + } + + protected ObjectMapper getMapper() { + return this.mapper; + } + + @Override + public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer td) + throws JacksonException { + + return super.deserialize(jp, ctxt); + } + + @Override + public T convert(JsonNode root, DeserializationContext ctxt) throws JacksonException { + Map headers = this.mapper.readValue(root.get("headers").traverse(ctxt), + this.mapper.getTypeFactory().constructMapType(HashMap.class, String.class, Object.class)); + Object payload = this.mapper.readValue(root.get("payload").traverse(ctxt), this.payloadType); + return buildMessage(new MutableMessageHeaders(headers), payload, root, ctxt); + } + + protected abstract T buildMessage(MutableMessageHeaders headers, Object payload, JsonNode root, + DeserializationContext ctxt) throws JacksonException; + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/MimeTypeJsonSerializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MimeTypeJsonSerializer.java new file mode 100644 index 0000000000..fffcd61acc --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MimeTypeJsonSerializer.java @@ -0,0 +1,53 @@ +/* + * 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 tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.ser.std.StdSerializer; + +import org.springframework.util.MimeType; + +/** + * Simple {@link StdSerializer} extension to represent a {@link MimeType} object in the + * target JSON as a plain string. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class MimeTypeJsonSerializer extends StdSerializer { + + public MimeTypeJsonSerializer() { + super(MimeType.class); + } + + @Override + public void serializeWithType(MimeType value, JsonGenerator gen, SerializationContext ctxt, + TypeSerializer typeSer) throws JacksonException { + + serialize(value, gen, ctxt); + } + + @Override + public void serialize(MimeType value, JsonGenerator gen, SerializationContext provider) throws JacksonException { + gen.writeString(value.toString()); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/MutableMessageJsonDeserializer.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MutableMessageJsonDeserializer.java new file mode 100644 index 0000000000..60d5d75478 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/MutableMessageJsonDeserializer.java @@ -0,0 +1,47 @@ +/* + * 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 tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; + +import org.springframework.integration.support.MutableMessage; +import org.springframework.integration.support.MutableMessageHeaders; + +/** + * The {@link MessageJsonDeserializer} implementation for the {@link MutableMessage}. + * + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +public class MutableMessageJsonDeserializer extends MessageJsonDeserializer> { + + @SuppressWarnings("unchecked") + public MutableMessageJsonDeserializer() { + super((Class>) (Class) MutableMessage.class); + } + + @Override + protected MutableMessage buildMessage(MutableMessageHeaders headers, Object payload, JsonNode root, + DeserializationContext ctxt) throws JacksonException { + + return new MutableMessage<>(payload, headers); + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/json/JacksonMessagingUtilsTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/json/JacksonMessagingUtilsTests.java new file mode 100644 index 0000000000..22806277e1 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/json/JacksonMessagingUtilsTests.java @@ -0,0 +1,143 @@ +/* + * 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.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.datatype.joda.JodaModule; +import tools.jackson.module.kotlin.KotlinModule; + +import org.springframework.integration.message.AdviceMessage; +import org.springframework.integration.support.MutableMessage; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jooyoung Pyoung + * + * @since 7.0 + */ +class JacksonMessagingUtilsTests { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = JacksonMessagingUtils.messagingAwareMapper(); + } + + @Test + void shouldIncludeWellKnownModules() { + List wellKnownModules = Arrays.asList( + new JodaModule(), + new KotlinModule.Builder().build() + ); + + Set wellKnownModuleNames = getModuleNames(wellKnownModules); + Set registeredModuleNames = getModuleNames(mapper.getRegisteredModules()); + + assertThat(registeredModuleNames).containsAll(wellKnownModuleNames); + } + + @Test + void shouldSerializeAndDeserializeGenericMessage() { + GenericMessage originalMessage = new GenericMessage<>("Hello World"); + + String json = mapper.writeValueAsString(originalMessage); + GenericMessage deserializedMessage = mapper.readValue(json, GenericMessage.class); + + assertThat(deserializedMessage).isNotNull(); + assertThat(deserializedMessage.getPayload()).isEqualTo("Hello World"); + } + + @Test + void shouldSerializeAndDeserializeErrorMessage() { + Exception exception = new RuntimeException("Test error"); + ErrorMessage originalMessage = new ErrorMessage(exception); + + String json = mapper.writeValueAsString(originalMessage); + ErrorMessage deserializedMessage = mapper.readValue(json, ErrorMessage.class); + + assertThat(deserializedMessage).isNotNull(); + assertThat(deserializedMessage.getPayload()).isInstanceOf(Throwable.class); + } + + @Test + void shouldSerializeAndDeserializeAdviceMessage() { + GenericMessage originalMessage = new GenericMessage<>("Original"); + AdviceMessage adviceMessage = new AdviceMessage<>("Advice payload", originalMessage); + + String json = mapper.writeValueAsString(adviceMessage); + @SuppressWarnings("unchecked") + AdviceMessage deserializedMessage = mapper.readValue(json, AdviceMessage.class); + + assertThat(deserializedMessage).isNotNull(); + assertThat(deserializedMessage.getPayload()).isEqualTo("Advice payload"); + } + + @Test + void shouldSerializeAndDeserializeMutableMessage() { + MutableMessage originalMessage = new MutableMessage<>("Mutable payload"); + + String json = mapper.writeValueAsString(originalMessage); + @SuppressWarnings("unchecked") + MutableMessage deserializedMessage = mapper.readValue(json, MutableMessage.class); + + assertThat(deserializedMessage).isNotNull(); + assertThat(deserializedMessage.getPayload()).isEqualTo("Mutable payload"); + } + + @Test + void shouldSerializeMessageHeaders() { + MessageHeaders headers = new MessageHeaders(null); + + String json = mapper.writeValueAsString(headers); + + assertThat(json).isNotNull(); + assertThat(json).contains("\"id\":"); + assertThat(json).contains("\"timestamp\":"); + } + + @Test + void shouldSerializeMimeType() { + MimeType mimeType = MimeType.valueOf("application/json"); + + String json = mapper.writeValueAsString(mimeType); + + assertThat(json).isNotNull(); + assertThat(json).contains("application\\/json"); + } + + private Set getModuleNames(Collection modules) { + return modules.stream() + .map(JacksonModule::getModuleName) + .collect(Collectors.toUnmodifiableSet()); + } + +} diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 863d0aed72..2acec7a171 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -184,7 +184,9 @@ - + + +