diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java index 88ef763b3..d66e45024 100644 --- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java +++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthConnector.java @@ -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) (json, typeOfT, context) -> { try { return Instant.parse(json.getAsString()); diff --git a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java index d54df5f53..0ed1a388f 100644 --- a/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java +++ b/bundles/org.openhab.core.auth.oauth2client/src/main/java/org/openhab/core/auth/oauth2client/internal/OAuthStoreHandlerImpl.java @@ -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 storage) { this.storage = storage; // Add adapters for Instant - gson = new GsonBuilder() + gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT) .registerTypeAdapter(Instant.class, (JsonDeserializer) (json, typeOfT, context) -> { try { return Instant.parse(json.getAsString()); diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java index 8a2e96e38..ef4e0b4f4 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/parser/gson/AbstractGSONParser.java @@ -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 implements Parser { // 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()) // diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/MediaTypeExtension.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/MediaTypeExtension.java index c1421092d..1830fd601 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/MediaTypeExtension.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/MediaTypeExtension.java @@ -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 implements MessageBodyReader, 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)); diff --git a/bundles/org.openhab.core.io.rest/src/test/java/org/openhab/core/io/rest/Stream2JSONInputStreamTest.java b/bundles/org.openhab.core.io.rest/src/test/java/org/openhab/core/io/rest/Stream2JSONInputStreamTest.java index 9a0559a66..e78632602 100644 --- a/bundles/org.openhab.core.io.rest/src/test/java/org/openhab/core/io/rest/Stream2JSONInputStreamTest.java +++ b/bundles/org.openhab.core.io.rest/src/test/java/org/openhab/core/io/rest/Stream2JSONInputStreamTest.java @@ -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 { diff --git a/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java b/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java index 983f588f1..cc79f526f 100644 --- a/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java +++ b/bundles/org.openhab.core.storage.json/src/main/java/org/openhab/core/storage/json/internal/JsonStorage.java @@ -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 implements Storage { 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()) // diff --git a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java index 5491294c0..285c06fce 100644 --- a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java +++ b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/JsonStorageTest.java @@ -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 diff --git a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/PersistedTransformationMigratorTest.java b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/PersistedTransformationMigratorTest.java index 1b8ea516c..b9ca903ec 100644 --- a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/PersistedTransformationMigratorTest.java +++ b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/PersistedTransformationMigratorTest.java @@ -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(); diff --git a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/ThingStorageEntityMigratorTest.java b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/ThingStorageEntityMigratorTest.java index 0326c9d90..e757c219a 100644 --- a/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/ThingStorageEntityMigratorTest.java +++ b/bundles/org.openhab.core.storage.json/src/test/java/org/openhab/core/storage/json/internal/ThingStorageEntityMigratorTest.java @@ -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(); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java index 848a28478..132ede449 100644 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/DateTimeType.java @@ -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"