GsonBuilder: Explicitly set date format (#4185)

Between Java 17 and Java 21, serialization of DateTime has changed due to
CLDR 42 which uses a narrow non-breaking space.
To ease switching JDK versions, the seralization format is explicitly
set to the Java 17 format.

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2024-04-28 17:11:54 +02:00 committed by GitHub
parent bad043ff12
commit fa9cff6be9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 72 additions and 9 deletions

View File

@ -39,6 +39,7 @@ import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.DateTimeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -83,7 +84,8 @@ public class OAuthConnector {
public OAuthConnector(HttpClientFactory httpClientFactory, @Nullable Fields extraFields, GsonBuilder gsonBuilder) {
this.httpClientFactory = httpClientFactory;
this.extraFields = extraFields;
gson = gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
gson = gsonBuilder.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, (JsonDeserializer<Instant>) (json, typeOfT, context) -> {
try {
return Instant.parse(json.getAsString());

View File

@ -35,6 +35,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.StorageCipher;
import org.openhab.core.auth.oauth2client.internal.cipher.SymmetricKeyCipher;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.StorageService;
import org.osgi.service.component.annotations.Activate;
@ -238,7 +239,7 @@ public class OAuthStoreHandlerImpl implements OAuthStoreHandler {
public StorageFacade(Storage<String> storage) {
this.storage = storage;
// Add adapters for Instant
gson = new GsonBuilder()
gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)
.registerTypeAdapter(Instant.class, (JsonDeserializer<Instant>) (json, typeOfT, context) -> {
try {
return Instant.parse(json.getAsString());

View File

@ -26,6 +26,7 @@ import org.openhab.core.config.core.ConfigurationDeserializer;
import org.openhab.core.config.core.ConfigurationSerializer;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;
import org.openhab.core.library.types.DateTimeType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -44,6 +45,7 @@ public abstract class AbstractGSONParser<T> implements Parser<T> {
// A Gson instance to use by the parsers
protected static Gson gson = new GsonBuilder() //
.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) //
.registerTypeAdapter(CompositeActionType.class, new ActionInstanceCreator()) //
.registerTypeAdapter(CompositeConditionType.class, new ConditionInstanceCreator()) //
.registerTypeAdapter(CompositeTriggerType.class, new TriggerInstanceCreator()) //

View File

@ -30,6 +30,7 @@ import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.library.types.DateTimeType;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
@ -61,7 +62,7 @@ public class MediaTypeExtension<T> implements MessageBodyReader<T>, MessageBodyW
* Constructor.
*/
public MediaTypeExtension() {
final Gson gson = new GsonBuilder().create();
final Gson gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT).create();
readers.put(mediaTypeWithoutParams(MediaType.APPLICATION_JSON_TYPE), new GsonMessageBodyReader<>(gson));
readers.put(mediaTypeWithoutParams(MediaType.TEXT_PLAIN_TYPE), new PlainMessageBodyReader<>());
writers.put(mediaTypeWithoutParams(MediaType.APPLICATION_JSON_TYPE), new GsonMessageBodyWriter<>(gson));

View File

@ -23,6 +23,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.types.DateTimeType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -35,7 +36,7 @@ import com.google.gson.GsonBuilder;
@NonNullByDefault
public class Stream2JSONInputStreamTest {
private static final Gson GSON = new GsonBuilder().create();
private static final Gson GSON = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT).create();
@Test
public void shouldReturnForEmptyStream() throws Exception {

View File

@ -37,6 +37,7 @@ import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.ConfigurationDeserializer;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.storage.Storage;
import org.openhab.core.storage.json.internal.migration.TypeMigrationException;
import org.openhab.core.storage.json.internal.migration.TypeMigrator;
@ -104,12 +105,14 @@ public class JsonStorage<T> implements Storage<T> {
this.typeMigrators = typeMigrators.stream().collect(Collectors.toMap(TypeMigrator::getOldType, e -> e));
this.internalMapper = new GsonBuilder() //
.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer())//
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer())//
.registerTypeHierarchyAdapter(Map.class, new StorageEntryMapDeserializer()) //
.setPrettyPrinting() //
.create();
this.entityMapper = new GsonBuilder() //
.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer())//
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer())//
.registerTypeAdapter(Configuration.class, new ConfigurationDeserializer()) //

View File

@ -20,7 +20,11 @@ import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@ -31,7 +35,10 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.test.java.JavaTest;
import com.google.gson.Gson;
@ -171,7 +178,7 @@ public class JsonStorageTest extends JavaTest {
}
String storageStringReserialized = Files.readString(tmpFile.toPath());
assertEquals(storageStringAB, storageStringReserialized);
Gson gson = new GsonBuilder().create();
Gson gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT).create();
// Parse json. Gson preserves json object key ordering when we parse only JsonObject
JsonObject orderedMap = gson.fromJson(storageStringAB, JsonObject.class);
@ -212,6 +219,46 @@ public class JsonStorageTest extends JavaTest {
.keySet().toArray());
}
@Test
@EnabledForJreRange(max = JRE.JAVA_19)
public void testDateSerialization17() {
// do NOT set format, setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)!
Gson gson = new GsonBuilder().create();
// generate a Date instance for 1-Jan-1980 0:00, compensating local time zone
ZonedDateTime zdt = ZonedDateTime.of(1980, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
Date date = Date.from(Instant.ofEpochSecond(zdt.toEpochSecond()));
// \u20af will encode to 3 bytes, 0xe280af
assertEquals("\"Jan 1, 1980, 12:00:00 AM\"", gson.toJson(date));
}
// CLDR 42 introduced in Java 20 changes serialization of Date, adding a narrow non-breakable space
@Test
@EnabledForJreRange(min = JRE.JAVA_20)
public void testDateSerialization21() {
// do NOT set format, setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)!
Gson gson = new GsonBuilder().create();
// generate a Date instance for 1-Jan-1980 0:00, compensating local time zone
ZonedDateTime zdt = ZonedDateTime.of(1980, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
Date date = Date.from(Instant.ofEpochSecond(zdt.toEpochSecond()));
// \u20af will encode to 3 bytes, 0xe280af
assertEquals("\"Jan 1, 1980, 12:00:00\u202fAM\"", gson.toJson(date));
}
// workaround, should work with Java 17 and 21
@Test
public void testDateSerialization() {
Gson gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT).create();
// generate a Date instance for 1-Jan-1980 0:00, compensating local time zone
ZonedDateTime zdt = ZonedDateTime.of(1980, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
Date date = Date.from(Instant.ofEpochSecond(zdt.toEpochSecond()));
assertEquals("\"Jan 1, 1980, 12:00:00 AM\"", gson.toJson(date));
}
private static class DummyObject {
// For the test here we use Linked variants of Map and Set which preserve the insertion order

View File

@ -30,6 +30,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.storage.json.internal.migration.PersistedTransformationTypeMigrator;
import org.openhab.core.storage.json.internal.migration.TypeMigrationException;
import org.openhab.core.storage.json.internal.migration.TypeMigrator;
@ -47,8 +48,9 @@ import com.google.gson.JsonElement;
public class PersistedTransformationMigratorTest {
private final Gson internalMapper = new GsonBuilder() //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer())//
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer())//
.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer()) //
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer()) //
.registerTypeHierarchyAdapter(Map.class, new StorageEntryMapDeserializer()) //
.setPrettyPrinting() //
.create();

View File

@ -31,6 +31,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.storage.json.internal.migration.BridgeImplTypeMigrator;
import org.openhab.core.storage.json.internal.migration.ThingImplTypeMigrator;
import org.openhab.core.storage.json.internal.migration.TypeMigrationException;
@ -49,8 +50,9 @@ import com.google.gson.JsonElement;
public class ThingStorageEntityMigratorTest {
private final Gson internalMapper = new GsonBuilder() //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer())//
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer())//
.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer()) //
.registerTypeHierarchyAdapter(Set.class, new OrderingSetSerializer()) //
.registerTypeHierarchyAdapter(Map.class, new StorageEntryMapDeserializer()) //
.setPrettyPrinting() //
.create();

View File

@ -48,6 +48,8 @@ public class DateTimeType implements PrimitiveType, State, Command {
public static final String DATE_PATTERN_WITH_TZ_AND_MS = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
public static final String DATE_PATTERN_WITH_TZ_AND_MS_GENERAL = "yyyy-MM-dd'T'HH:mm:ss.SSSz";
public static final String DATE_PATTERN_WITH_TZ_AND_MS_ISO = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
// serialization of Date, Java 17 compatible format
public static final String DATE_PATTERN_JSON_COMPAT = "MMM d, yyyy, h:mm:ss aaa";
// internal patterns for parsing
private static final String DATE_PARSE_PATTERN_WITHOUT_TZ = "yyyy-MM-dd'T'HH:mm"