diff --git a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java index 71c2239c4..266bf9494 100755 --- a/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java +++ b/extensions/jackson/deployment/src/main/java/io/quarkus/jackson/deployment/JacksonProcessor.java @@ -232,6 +232,11 @@ public class JacksonProcessor { customize.returnValue(null); } + + // ensure that the things we auto-register have the lower priority - this ensures that user registered modules take priority + try (MethodCreator priority = classCreator.getMethodCreator("priority", int.class)) { + priority.returnValue(priority.load(ObjectMapperCustomizer.MINIMUM_PRIORITY)); + } } } } diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperCustomizer.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperCustomizer.java index 789d4625d..0aac835a0 100644 --- a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperCustomizer.java +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperCustomizer.java @@ -10,7 +10,22 @@ import com.fasterxml.jackson.databind.ObjectMapper; *

* See also {@link ObjectMapperProducer#objectMapper}. */ -public interface ObjectMapperCustomizer { +public interface ObjectMapperCustomizer extends Comparable { + + int MINIMUM_PRIORITY = Integer.MIN_VALUE; + int DEFAULT_PRIORITY = 0; void customize(ObjectMapper objectMapper); + + /** + * Defines the priority that the customizers are applied. + * A lower integer value means that the customizer will be applied after a customizer with a higher priority + */ + default int priority() { + return DEFAULT_PRIORITY; + } + + default int compareTo(ObjectMapperCustomizer o) { + return Integer.compare(o.priority(), priority()); + } } diff --git a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java index ddc397f7e..c017ba5e7 100644 --- a/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java +++ b/extensions/jackson/runtime/src/main/java/io/quarkus/jackson/ObjectMapperProducer.java @@ -1,5 +1,9 @@ package io.quarkus.jackson; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Instance; import javax.enterprise.inject.Produces; @@ -17,9 +21,20 @@ public class ObjectMapperProducer { @Produces public ObjectMapper objectMapper(Instance customizers) { ObjectMapper objectMapper = new ObjectMapper(); - for (ObjectMapperCustomizer customizer : customizers) { + List sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers); + for (ObjectMapperCustomizer customizer : sortedCustomizers) { customizer.customize(objectMapper); } return objectMapper; } + + private List sortCustomizersInDescendingPriorityOrder( + Instance customizers) { + List sortedCustomizers = new ArrayList<>(); + for (ObjectMapperCustomizer customizer : customizers) { + sortedCustomizers.add(customizer); + } + Collections.sort(sortedCustomizers); + return sortedCustomizers; + } } diff --git a/extensions/resteasy-jackson/deployment/pom.xml b/extensions/resteasy-jackson/deployment/pom.xml index 689259d98..eeee12900 100644 --- a/extensions/resteasy-jackson/deployment/pom.xml +++ b/extensions/resteasy-jackson/deployment/pom.xml @@ -26,6 +26,16 @@ io.quarkus quarkus-resteasy-jackson + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + diff --git a/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/DateDto.java b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/DateDto.java new file mode 100644 index 000000000..bc3022089 --- /dev/null +++ b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/DateDto.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.jackson; + +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DateDto { + @JsonProperty("current_date") + private ZonedDateTime currentDate; + + public void setCurrentDate(ZonedDateTime currentDate) { + this.currentDate = currentDate; + } + + public ZonedDateTime getCurrentDate() { + return currentDate; + } + + public DateDto(ZonedDateTime currentDate) { + this.currentDate = currentDate; + } +} diff --git a/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/HelloResource.java b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/HelloResource.java new file mode 100644 index 000000000..924a6ca63 --- /dev/null +++ b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/HelloResource.java @@ -0,0 +1,18 @@ +package io.quarkus.resteasy.jackson; + +import java.time.ZonedDateTime; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Produces(MediaType.APPLICATION_JSON) +@Path("/hello") +public class HelloResource { + + @GET + public DateDto hello() { + return new DateDto(ZonedDateTime.now()); + } +} diff --git a/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/MultipleTimeModuleTest.java b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/MultipleTimeModuleTest.java new file mode 100644 index 000000000..5d78b59f6 --- /dev/null +++ b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/MultipleTimeModuleTest.java @@ -0,0 +1,47 @@ +package io.quarkus.resteasy.jackson; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +// this test really belongs in the jackson module, but it's been added here to avoid test classpath issues +public class MultipleTimeModuleTest { + + @RegisterExtension + static QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TimeCustomizer.class, DateDto.class, HelloResource.class)); + + @Test + public void testDateIsAlwaysInTheExpectedFormat() { + verifyExpectedResult(); + + modifyResource(); + verifyExpectedResult(); + + modifyResource(); + verifyExpectedResult(); + + modifyResource(); + verifyExpectedResult(); + } + + private void verifyExpectedResult() { + RestAssured.get("/hello").then() + .statusCode(200) + .body(containsString("Z"), not(containsString("+"))); + } + + private void modifyResource() { + TEST.modifySourceFile("TimeCustomizer.java", s -> s.replace("hello", + "hello2")); + } + +} diff --git a/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/TimeCustomizer.java b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/TimeCustomizer.java new file mode 100644 index 000000000..8d2678140 --- /dev/null +++ b/extensions/resteasy-jackson/deployment/src/test/java/io/quarkus/resteasy/jackson/TimeCustomizer.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.jackson; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatterBuilder; + +import javax.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.ser.ZonedDateTimeSerializer; + +import io.quarkus.jackson.ObjectMapperCustomizer; + +@Singleton +public class TimeCustomizer implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + JavaTimeModule customDateModule = new JavaTimeModule(); + customDateModule.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer( + new DateTimeFormatterBuilder().appendInstant(0).toFormatter().withZone(ZoneId.of("Z")))); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .registerModule(customDateModule); + } +}