diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/controller/PublishingPayloadCreator.java b/springwolf-core/src/main/java/io/github/springwolf/core/controller/PublishingPayloadCreator.java index 65f494be2..8e0e412b7 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/controller/PublishingPayloadCreator.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/controller/PublishingPayloadCreator.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; +import io.github.springwolf.asyncapi.v3.model.schema.SchemaType; import io.github.springwolf.core.asyncapi.components.ComponentsService; import io.github.springwolf.core.controller.dtos.MessageDto; import jakarta.annotation.Nullable; @@ -11,6 +13,7 @@ import org.apache.commons.lang3.StringUtils; import java.text.MessageFormat; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -32,17 +35,20 @@ public Result createPayloadObject(MessageDto message) { return new Result(null, Optional.empty()); } - Set knownSchemaNames = componentsService.getSchemas().keySet(); - for (String schemaPayloadType : knownSchemaNames) { + Map knownSchemas = componentsService.getSchemas(); + Set knownSchemaNames = knownSchemas.keySet(); + for (Map.Entry schemaEntry : knownSchemas.entrySet()) { + String schemaName = schemaEntry.getKey(); + SchemaObject schema = schemaEntry.getValue(); + // security: match against user input, but always use our controlled data from the DefaultSchemaService - if (schemaPayloadType != null && schemaPayloadType.equals(messagePayloadType)) { + if (schemaName != null && schemaName.equals(messagePayloadType)) { try { - Class payloadClass = Class.forName(schemaPayloadType); - Object payload = objectMapper.readValue(message.getPayload(), payloadClass); + Object payload = resolveActualPayload(message, schema, schemaName); return new Result(payload, Optional.empty()); - } catch (ClassNotFoundException | JsonProcessingException ex) { + } catch (ClassNotFoundException | JsonProcessingException | IllegalArgumentException ex) { String errorMessage = MessageFormat.format( - "Unable to create payload {0} from data: {1}", schemaPayloadType, message.getPayload()); + "Unable to create payload {0} from data: {1}", schemaName, message.getPayload()); log.info(errorMessage, ex); return new Result(null, Optional.of(errorMessage)); } @@ -57,5 +63,28 @@ public Result createPayloadObject(MessageDto message) { return new Result(null, Optional.of(errorMessage)); } + private Object resolveActualPayload(MessageDto message, SchemaObject schema, String schemaName) + throws ClassNotFoundException, JsonProcessingException, IllegalArgumentException { + switch (schema.getType()) { + case SchemaType.BOOLEAN -> { + return objectMapper.readValue(message.getPayload(), Boolean.class); + } + case SchemaType.INTEGER -> { + return objectMapper.readValue(message.getPayload(), Long.class); + } + case SchemaType.NUMBER -> { + return objectMapper.readValue(message.getPayload(), Double.class); + } + case SchemaType.OBJECT -> { + Class payloadClass = Class.forName(schemaName); + return objectMapper.readValue(message.getPayload(), payloadClass); + } + case SchemaType.STRING -> { + return objectMapper.readValue(message.getPayload(), String.class); + } + default -> throw new IllegalArgumentException("Unsupported schema type: " + schema.getType()); + } + } + public record Result(@Nullable Object payload, Optional errorMessage) {} } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java b/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java index 74cc03e20..16d46a6f7 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/controller/dtos/MessageDto.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.core.controller.dtos; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.extern.jackson.Jacksonized; @@ -10,6 +11,7 @@ @Data @Builder @Jacksonized +@AllArgsConstructor public class MessageDto { public static final String EMPTY = ""; diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/controller/PublishingPayloadCreatorTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/controller/PublishingPayloadCreatorTest.java new file mode 100644 index 000000000..3c4d3b950 --- /dev/null +++ b/springwolf-core/src/test/java/io/github/springwolf/core/controller/PublishingPayloadCreatorTest.java @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.springwolf.core.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject; +import io.github.springwolf.asyncapi.v3.model.schema.SchemaType; +import io.github.springwolf.core.asyncapi.components.ComponentsService; +import io.github.springwolf.core.controller.dtos.MessageDto; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PublishingPayloadCreatorTest { + + @Mock + private ComponentsService componentsService; + + @Mock + private ObjectMapper objectMapper; + + @InjectMocks + private PublishingPayloadCreator publishingPayloadCreator; + + @Test + void shouldResolveEmptyPayload() { + // given + String payloadType = ObjectClass.class.getName(); + String payload = ""; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(null, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveBooleanPayload() throws JsonProcessingException { + // given + String payloadType = Boolean.class.getName(); + String payload = "true"; + Boolean payloadTyped = true; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.BOOLEAN).build())); + when(objectMapper.readValue(payload, Boolean.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveIntegerPayload() throws JsonProcessingException { + // given + String payloadType = Integer.class.getName(); + String payload = "12345678"; + Long payloadTyped = 12345678L; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.INTEGER).build())); + when(objectMapper.readValue(payload, Long.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveLongPayload() throws JsonProcessingException { + // given + String payloadType = Long.class.getName(); + String payload = Long.valueOf(Long.MAX_VALUE).toString(); + Long payloadTyped = Long.MAX_VALUE; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.INTEGER).build())); + when(objectMapper.readValue(payload, Long.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveFloatPayload() throws JsonProcessingException { + // given + String payloadType = Float.class.getName(); + String payload = "12345678.123"; + Double payloadTyped = 12345678.123; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.NUMBER).build())); + when(objectMapper.readValue(payload, Double.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveDoublePayload() throws JsonProcessingException { + // given + String payloadType = Double.class.getName(); + String payload = Double.valueOf(Double.MAX_VALUE).toString(); + Double payloadTyped = Double.MAX_VALUE; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.NUMBER).build())); + when(objectMapper.readValue(payload, Double.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveObjectPayload() throws JsonProcessingException { + // given + String payloadType = ObjectClass.class.getName(); + String payload = "{\"value\":\"test\"}"; + ObjectClass payloadTyped = new ObjectClass("test"); + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.OBJECT).build())); + when(objectMapper.readValue(payload, ObjectClass.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldResolveStringPayload() throws JsonProcessingException { + // given + String payloadType = String.class.getName(); + String payload = "\"test\""; + String payloadTyped = "test"; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.STRING).build())); + when(objectMapper.readValue(payload, String.class)).thenReturn(payloadTyped); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(payloadTyped, result.payload()); + assertEquals(Optional.empty(), result.errorMessage()); + } + + @Test + void shouldReturnEmptyPayloadForUnknownClass() { + // given + String payloadType = "MyUnknownClass"; + String payload = "payload-data"; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.OBJECT).build())); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(null, result.payload()); + assertEquals( + Optional.of("Unable to create payload MyUnknownClass from data: " + payload), result.errorMessage()); + } + + @Test + void shouldReturnEmptyPayloadForInvalidJson() throws JsonProcessingException { + // given + String payloadType = ObjectClass.class.getName(); + String payload = "---invalid-json---"; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.OBJECT).build())); + when(objectMapper.readValue(payload, ObjectClass.class)) + .thenThrow(new JsonProcessingException("invalid json") {}); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(null, result.payload()); + assertEquals( + Optional.of( + "Unable to create payload io.github.springwolf.core.controller.PublishingPayloadCreatorTest$ObjectClass from data: " + + payload), + result.errorMessage()); + } + + @Test + void shouldReturnEmptyPayloadForUnsupportedSchemaType() { + // given + String payloadType = ObjectClass.class.getName(); + String payload = "{}"; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + when(componentsService.getSchemas()) + .thenReturn(Map.of( + payloadType, + SchemaObject.builder().type(SchemaType.ARRAY).build())); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(null, result.payload()); + assertEquals( + Optional.of( + "Unable to create payload io.github.springwolf.core.controller.PublishingPayloadCreatorTest$ObjectClass from data: " + + payload), + result.errorMessage()); + } + + @Test + void shouldReturnEmptyPayloadForUnmatchedPayloadType() { + // given + String payloadType = String.class.getName(); + String payload = "{\"value\":\"test\"}"; + MessageDto message = new MessageDto(Map.of(), Map.of(), payloadType, payload); + + // when + PublishingPayloadCreator.Result result = publishingPayloadCreator.createPayloadObject(message); + + // then + assertNotNull(result); + assertEquals(null, result.payload()); + assertEquals( + Optional.of("Specified payloadType java.lang.String is not a registered springwolf schema."), + result.errorMessage()); + } + + record ObjectClass(String value) {} +}