Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow configuring Jackson's TimeZone via configuration #15431

Merged
merged 2 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public Holder(Class<F> from, Class<T> to, Class<ObjectSubstitution<F, T>> substi

public final Holder<?, ?> holder;

public <F, T> ObjectSubstitutionBuildItem(Class<F> from, Class<T> to, Class<ObjectSubstitution<F, T>> substitution) {
holder = new Holder<>(from, to, substitution);
public <F, T> ObjectSubstitutionBuildItem(Class<F> from, Class<T> to,
Class<? extends ObjectSubstitution<F, T>> substitution) {
holder = new Holder(from, to, substitution);
}

public ObjectSubstitutionBuildItem(Holder<?, ?> holder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.deployment.recording.substitutions;

import java.time.ZoneId;

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ObjectSubstitutionBuildItem;
import io.quarkus.runtime.recording.substitutions.ZoneIdSubstitution;

public class AdditionalSubstitutionsBuildStep {

@BuildStep
public void additionalSubstitutions(BuildProducer<ObjectSubstitutionBuildItem> producer) {
zoneIdSubstitutions(producer);
}

@SuppressWarnings("unchecked")
private void zoneIdSubstitutions(BuildProducer<ObjectSubstitutionBuildItem> producer) {
try {
/*
* We can't refer to these classes as they are package private but we need a handle on need
* because the bytecode recorder needs to have the actual class registered and not a super class
*/

Class<ZoneId> zoneRegionClass = (Class<ZoneId>) Class.forName("java.time.ZoneRegion");
producer.produce(new ObjectSubstitutionBuildItem(zoneRegionClass, String.class, ZoneIdSubstitution.class));

Class<ZoneId> zoneOffsetClass = (Class<ZoneId>) Class.forName("java.time.ZoneOffset");
producer.produce(new ObjectSubstitutionBuildItem(zoneOffsetClass, String.class, ZoneIdSubstitution.class));
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Improper registration of ZoneId substitution", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.runtime.configuration;

import static io.quarkus.runtime.configuration.ConverterSupport.DEFAULT_QUARKUS_CONVERTER_PRIORITY;

import java.io.Serializable;
import java.time.ZoneId;

import javax.annotation.Priority;

import org.eclipse.microprofile.config.spi.Converter;

/**
* A converter to support ZoneId.
*/
@Priority(DEFAULT_QUARKUS_CONVERTER_PRIORITY)
public class ZoneIdConverter implements Converter<ZoneId>, Serializable {

private static final long serialVersionUID = -439010527617997936L;

@Override
public ZoneId convert(final String value) {
return ZoneId.of(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.runtime.recording.substitutions;

import java.time.ZoneId;

import io.quarkus.runtime.ObjectSubstitution;

public class ZoneIdSubstitution implements ObjectSubstitution<ZoneId, String> {

@Override
public String serialize(ZoneId obj) {
return obj.getId();
}

@Override
public ZoneId deserialize(String str) {
return ZoneId.of(str);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ io.quarkus.runtime.configuration.PathConverter
io.quarkus.runtime.configuration.DurationConverter
io.quarkus.runtime.configuration.MemorySizeConverter
io.quarkus.runtime.configuration.LocaleConverter
io.quarkus.runtime.configuration.ZoneIdConverter
io.quarkus.runtime.logging.LevelConverter
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkus.jackson.deployment;

import static org.junit.jupiter.api.Assertions.fail;

import java.time.zone.ZoneRulesException;
import java.util.Date;

import javax.inject.Inject;
import javax.inject.Singleton;

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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.test.QuarkusUnitTest;

public class JacksonErroneousTimeZonePropertiesTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(Pojo.class, SomeBean.class))
.withConfigurationResource("application-erroneous-timezone-properties.properties")
.setExpectedException(ZoneRulesException.class);

@Test
public void test() {
fail("Should never have been called");
}

@Singleton
public static class SomeBean {

@Inject
ObjectMapper objectMapper;

public String write(Pojo pojo) throws JsonProcessingException {
return objectMapper.writeValueAsString(pojo);
}

}

public static class Pojo {

private final Date date;

public Pojo(Date date) {
this.date = date;
}

public Date getDate() {
return date;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.jackson.deployment;

import java.util.Calendar;
import java.util.Date;

import javax.inject.Inject;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.test.QuarkusUnitTest;

public class JacksonTimeZonePropertiesTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("application-timezone-properties.properties");

@Inject
ObjectMapper objectMapper;

@Test
public void testTimezone() throws JsonProcessingException {
Assertions.assertThat(objectMapper.writeValueAsString(new Pojo(new Date(2021, Calendar.MARCH, 3, 11, 5))))
.contains("+07");
}

public static class Pojo {

private final Date date;

public Pojo(Date date) {
this.date = date;
}

public Date getDate() {
return date;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.jackson.timezone=dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quarkus.jackson.timezone=Asia/Jakarta
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkus.jackson.runtime;

import java.time.ZoneId;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;

Expand All @@ -19,4 +22,12 @@ public class JacksonBuildTimeConfig {
*/
@ConfigItem(defaultValue = "false")
public boolean writeDatesAsTimestamps;

/**
* If set, Jackson will default to using the specified timezone when formatting dates.
* Some examples values are "Asia/Jakarta" and "GMT+3".
* If not set, Jackson will use its own default.
*/
@ConfigItem(defaultValue = "UTC")
public Optional<ZoneId> timezone;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package io.quarkus.jackson.runtime;

import java.time.ZoneId;

public class JacksonConfigSupport {

private final boolean failOnUnknownProperties;

private final boolean writeDatesAsTimestamps;

public JacksonConfigSupport(boolean failOnUnknownProperties, boolean writeDatesAsTimestamps) {
private final ZoneId timeZone;

public JacksonConfigSupport(boolean failOnUnknownProperties, boolean writeDatesAsTimestamps, ZoneId timeZone) {
this.failOnUnknownProperties = failOnUnknownProperties;
this.writeDatesAsTimestamps = writeDatesAsTimestamps;
this.timeZone = timeZone;
}

public boolean isFailOnUnknownProperties() {
Expand All @@ -18,4 +23,8 @@ public boolean isFailOnUnknownProperties() {
public boolean isWriteDatesAsTimestamps() {
return writeDatesAsTimestamps;
}

public ZoneId getTimeZone() {
return timeZone;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public Supplier<JacksonConfigSupport> jacksonConfigSupport(JacksonBuildTimeConfi
@Override
public JacksonConfigSupport get() {
return new JacksonConfigSupport(jacksonBuildTimeConfig.failOnUnknownProperties,
jacksonBuildTimeConfig.writeDatesAsTimestamps);
jacksonBuildTimeConfig.writeDatesAsTimestamps, jacksonBuildTimeConfig.timezone.orElse(null));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.quarkus.jackson.runtime;

import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
Expand Down Expand Up @@ -33,6 +35,10 @@ public ObjectMapper objectMapper(Instance<ObjectMapperCustomizer> customizers,
// this feature is enabled by default, so we disable it
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
ZoneId zoneId = jacksonConfigSupport.getTimeZone();
if ((zoneId != null) && !zoneId.getId().equals("UTC")) { // Jackson uses UTC as the default, so let's not reset it
objectMapper.setTimeZone(TimeZone.getTimeZone(zoneId));
}
List<ObjectMapperCustomizer> sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers);
for (ObjectMapperCustomizer customizer : sortedCustomizers) {
customizer.customize(objectMapper);
Expand Down