diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DmfApiConfiguration.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DmfApiConfiguration.java index ad1613ef4..48cfb33cf 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DmfApiConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/DmfApiConfiguration.java @@ -27,6 +27,10 @@ import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; @@ -34,6 +38,8 @@ import org.springframework.amqp.rabbit.listener.FatalExceptionStrategy; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -89,13 +95,19 @@ public class DmfApiConfiguration { return new ConditionalRejectingErrorHandler(new RequeueExceptionStrategy(fatalExceptionStrategies, fatalExceptionTypes)); } + @Bean + @ConditionalOnMissingBean(name = "amqpMessageConverter") // override it if needed to add / edit trusted packages or need other customization + public MessageConverter amqpMessageConverter(final JsonMapper jsonMapper) { + return DmfApiConfiguration.messageConverter(jsonMapper); + } + /** * @return {@link RabbitTemplate} with automatic retry, published confirms and {@link JacksonJsonMessageConverter}. */ @Bean - public RabbitTemplate rabbitTemplate(final JsonMapper jsonMapper) { + public RabbitTemplate rabbitTemplate(@Qualifier("amqpMessageConverter") final MessageConverter amqpMessageConverter) { final RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory); - rabbitTemplate.setMessageConverter(new JacksonJsonMessageConverter(jsonMapper)); + rabbitTemplate.setMessageConverter(amqpMessageConverter); // the same policy the previously used default ExponentialBackOffPolicy applied rabbitTemplate.setRetryTemplate(new RetryTemplate(RetryPolicy.builder() @@ -168,12 +180,38 @@ public class DmfApiConfiguration { final SystemManagement systemManagement, final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement, - final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement) { + final SoftwareModuleManagement softwareModuleManagement, + final DeploymentManagement deploymentManagement) { return new AmqpMessageDispatcherService(rabbitTemplate, amqpSenderService, artifactUrlHandler, systemManagement, targetManagement, softwareModuleManagement, distributionSetManagement, deploymentManagement); } + // since spring-amqp 4.0.4 not all packages are assumed trusted for type converter (only java.land and java.util) + // so e need to add hawkbit (and eventual extension packages as trusted + // also (again since spring-amqp 4.0.4) the conversion from empty payload fail (which probably is fine since it is JSON) + // however, (for backward compatibility, e.g. THING_REMOVED doesn't define payload and could be empty byte[]) we assume that + // empty payload is empty byte[] and not try to convert it to Object (which fail since it is not JSON) + static @NonNull JacksonJsonMessageConverter messageConverter(final JsonMapper jsonMapper) { + return messageConverter(jsonMapper, "org.eclipse.hawkbit.dmf.json.model"); + } + + public static @NonNull JacksonJsonMessageConverter messageConverter(final JsonMapper jsonMapper, final String... trustedPackages) { + return new JacksonJsonMessageConverter(jsonMapper, trustedPackages) { + + @Override + public @NonNull Object fromMessage(@NonNull final Message message, final @Nullable Object conversionHint) { + // default converter tries to convert empty body payload to Object (since rabbit 4.0.4) + // which probably is correct since it has to be JSON - however, in this case we assume - empty byte[] + if (message.getBody().length == 0) { + return message.getBody(); + } else { + return super.fromMessage(message, conversionHint); + } + } + }; + } + @ToString private static class SqlFatalExceptionStrategy implements FatalExceptionStrategy { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java index 1c53eb502..d1afe741e 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageType; import org.eclipse.hawkbit.dmf.json.model.DmfActionRequest; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadAndUpdateRequest; import org.eclipse.hawkbit.dmf.json.model.DmfSoftwareModule; +import org.eclipse.hawkbit.rabbitmq.test.AmqpTestConfiguration; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TargetManagement.Create; import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; @@ -60,10 +61,10 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.DefaultJacksonJavaTypeMapper; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.TestPropertySource; +import tools.jackson.databind.json.JsonMapper; /** * Feature: Component Tests - Device Management Federation API
@@ -94,7 +95,7 @@ class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { Create.builder().controllerId(CONTROLLER_ID).securityToken(TEST_TOKEN).address(AMQP_URI.toString()).build()); this.rabbitTemplate = Mockito.mock(RabbitTemplate.class); - when(rabbitTemplate.getMessageConverter()).thenReturn(new JacksonJsonMessageConverter()); + when(rabbitTemplate.getMessageConverter()).thenReturn(AmqpTestConfiguration.messageConverter(new JsonMapper())); senderService = Mockito.mock(DefaultAmqpMessageSenderService.class); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index ca0578c4b..bfa73103e 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -54,9 +54,9 @@ import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; +import tools.jackson.databind.json.JsonMapper; /** * Feature: Component Tests - Device Management Federation API
@@ -108,7 +108,7 @@ class AmqpMessageHandlerServiceTest { @SuppressWarnings({ "rawtypes", "unchecked" }) void before() { TenantConfigHelper.setTenantConfigurationManagement(tenantConfigurationManagement); - messageConverter = new JacksonJsonMessageConverter(); + messageConverter = DmfApiConfiguration.messageConverter(new JsonMapper()); lenient().when(rabbitTemplate.getMessageConverter()).thenReturn(messageConverter); amqpMessageHandlerService = new AmqpMessageHandlerService( diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java index 5f8a794b4..bb1cb4067 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java @@ -28,8 +28,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; +import tools.jackson.databind.json.JsonMapper; /** * Feature: Component Tests - Device Management Federation API
@@ -54,7 +54,7 @@ class BaseAmqpServiceTest { @Test void convertMessageTest() { final DmfActionUpdateStatus actionUpdateStatus = createActionStatus(); - when(rabbitTemplate.getMessageConverter()).thenReturn(new JacksonJsonMessageConverter()); + when(rabbitTemplate.getMessageConverter()).thenReturn(DmfApiConfiguration.messageConverter(new JsonMapper())); final Message message = rabbitTemplate.getMessageConverter().toMessage(actionUpdateStatus, createJsonProperties()); final DmfActionUpdateStatus convertedActionUpdateStatus = baseAmqpService.convertMessage(message, DmfActionUpdateStatus.class); @@ -91,7 +91,7 @@ class BaseAmqpServiceTest { @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) }) void updateActionStatusWithInvalidJsonContent() { final Message message = createMessage("Invalid Json".getBytes()); - when(rabbitTemplate.getMessageConverter()).thenReturn(new JacksonJsonMessageConverter()); + when(rabbitTemplate.getMessageConverter()).thenReturn(DmfApiConfiguration.messageConverter(new JsonMapper())); assertThatExceptionOfType(MessageConversionException.class) .as("Expected MessageConversionException for invalid JSON") diff --git a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java index a4e2e8e17..26063579d 100644 --- a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java @@ -25,12 +25,12 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; +import tools.jackson.databind.json.JsonMapper; @Slf4j @RabbitAvailable @@ -88,7 +88,7 @@ public abstract class AbstractAmqpIntegrationTest extends AbstractIntegrationTes private RabbitTemplate createDmfClient() { final RabbitTemplate template = new RabbitTemplate(connectionFactory); - template.setMessageConverter(new JacksonJsonMessageConverter()); + template.setMessageConverter(AmqpTestConfiguration.messageConverter(new JsonMapper())); template.setReceiveTimeout(TimeUnit.SECONDS.toMillis(3)); template.setReplyTimeout(TimeUnit.SECONDS.toMillis(3)); template.setExchange(getExchange()); diff --git a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AmqpTestConfiguration.java b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AmqpTestConfiguration.java index 90015ad2a..b346839ec 100644 --- a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AmqpTestConfiguration.java +++ b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AmqpTestConfiguration.java @@ -14,6 +14,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; @@ -24,6 +27,7 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService; +import tools.jackson.databind.json.JsonMapper; @Configuration public class AmqpTestConfiguration { @@ -32,7 +36,7 @@ public class AmqpTestConfiguration { @Primary public RabbitTemplate rabbitTemplateForTest(final ConnectionFactory connectionFactory) { final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); - rabbitTemplate.setMessageConverter(new JacksonJsonMessageConverter()); + rabbitTemplate.setMessageConverter(messageConverter(new JsonMapper())); rabbitTemplate.setReplyTimeout(TimeUnit.SECONDS.toMillis(3)); rabbitTemplate.setReceiveTimeout(TimeUnit.SECONDS.toMillis(3)); return rabbitTemplate; @@ -67,4 +71,19 @@ public class AmqpTestConfiguration { RabbitMqSetupService rabbitMqSetupService() { return new RabbitMqSetupService(); } + + // note - it MUST be the same as DmfApiConfiguration#messageConverter for the test to work properly (to test the real AMQP) + public static @NonNull JacksonJsonMessageConverter messageConverter(final JsonMapper jsonMapper) { + return new JacksonJsonMessageConverter(jsonMapper, "org.eclipse.hawkbit.dmf.json.model") { + + @Override + public @NonNull Object fromMessage(@NonNull final Message message, final @Nullable Object conversionHint) { + if (message.getBody().length == 0) { + return message.getBody(); + } else { + return super.fromMessage(message, conversionHint); + } + } + }; + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index c34425484..e420051fb 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.6 + 4.0.7 org.eclipse.hawkbit @@ -58,7 +58,7 @@ 17 - 4.0.6 + 4.0.7 2025.1.1 1.1.7 3.0.3