diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index ebe4a1b05e2..9d8e666a0ff 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -13,6 +13,7 @@ package org.openhab.core.io.rest.core.internal.item; import java.time.Instant; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; @@ -57,6 +58,7 @@ import org.openhab.core.auth.Role; import org.openhab.core.common.registry.RegistryChangedRunnableListener; import org.openhab.core.events.EventPublisher; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.DTOMapper; import org.openhab.core.io.rest.JSONResponse; import org.openhab.core.io.rest.LocaleService; @@ -180,6 +182,7 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; private final SemanticTagRegistry semanticTagRegistry; + private final TimeZoneProvider timeZoneProvider; private final RegistryChangedRunnableListener resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>( () -> lastModified = null); @@ -198,7 +201,8 @@ public ItemResource(// final @Reference ManagedItemProvider managedItemProvider, final @Reference MetadataRegistry metadataRegistry, final @Reference MetadataSelectorMatcher metadataSelectorMatcher, - final @Reference SemanticTagRegistry semanticTagRegistry) { + final @Reference SemanticTagRegistry semanticTagRegistry, + final @Reference TimeZoneProvider timeZoneProvider) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; this.itemBuilderFactory = itemBuilderFactory; @@ -208,6 +212,7 @@ public ItemResource(// this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; this.semanticTagRegistry = semanticTagRegistry; + this.timeZoneProvider = timeZoneProvider; this.itemRegistry.addRegistryChangeListener(resetLastModifiedItemChangeListener); this.metadataRegistry.addRegistryChangeListener(resetLastModifiedMetadataChangeListener); @@ -240,6 +245,7 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead @QueryParam("fields") @Parameter(description = "limit output to the given fields (comma separated)") @Nullable String fields, @DefaultValue("false") @QueryParam("staticDataOnly") @Parameter(description = "provides a cacheable list of values not expected to change regularly and checks the If-Modified-Since header, all other parameters are ignored except \"metadata\"") boolean staticDataOnly) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders); @@ -256,7 +262,7 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead } Stream itemStream = getItems(type, tags).stream() // - .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale)) // + .map(item -> EnrichedItemDTOMapper.map(item, false, null, uriBuilder, locale, zoneId)) // .peek(dto -> addMetadata(dto, namespaces, null)) // .peek(dto -> dto.editable = isEditable(dto.name)); itemStream = dtoMapper.limitToFields(itemStream, @@ -267,7 +273,7 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead } Stream itemStream = getItems(type, tags).stream() // - .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) // + .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale, zoneId)) // .peek(dto -> addMetadata(dto, namespaces, null)) // .peek(dto -> dto.editable = isEditable(dto.name)) // .peek(dto -> { @@ -318,6 +324,7 @@ public Response getItemByName(final @Context UriInfo uriInfo, final @Context Htt @DefaultValue("true") @QueryParam("recursive") @Parameter(description = "get member items if the item is a group item") boolean recursive, @PathParam("itemname") @Parameter(description = "item name") String itemname) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); // get item @@ -326,7 +333,7 @@ public Response getItemByName(final @Context UriInfo uriInfo, final @Context Htt // if it exists if (item != null) { EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder(uriInfo, httpHeaders), - locale); + locale, zoneId); addMetadata(dto, namespaces, null); dto.editable = isEditable(dto.name); if (dto instanceof EnrichedGroupItemDTO enrichedGroupItemDTO) { @@ -424,6 +431,7 @@ public Response putItemState( @PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "valid item state (e.g. ON, OFF)", required = true) String value) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); // get Item Item item = getItem(itemname); @@ -436,7 +444,7 @@ public Response putItemState( if (state != null) { // set State and report OK eventPublisher.post(ItemEventFactory.createStateEvent(itemname, state)); - return getItemResponse(null, Status.ACCEPTED, null, locale, null); + return getItemResponse(null, Status.ACCEPTED, null, locale, zoneId, null); } else { // State could not be parsed return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "State could not be parsed: " + value); @@ -739,6 +747,7 @@ public Response createOrUpdateItem(final @Context UriInfo uriInfo, final @Contex @PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "item data", required = true) @Nullable GroupItemDTO item) { final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); // If we didn't get an item bean, then return! if (item == null) { @@ -763,12 +772,12 @@ public Response createOrUpdateItem(final @Context UriInfo uriInfo, final @Contex // item does not yet exist, create it managedItemProvider.add(newItem); return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.CREATED, itemRegistry.get(itemname), - locale, null); + locale, zoneId, null); } else if (managedItemProvider.get(itemname) != null) { // item already exists as a managed item, update it managedItemProvider.update(newItem); return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.OK, itemRegistry.get(itemname), locale, - null); + zoneId, null); } else { // Item exists but cannot be updated logger.warn("Cannot update existing item '{}', because is not managed.", itemname); @@ -872,7 +881,8 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @PathParam("itemName") @Parameter(description = "item name") String itemName, @PathParam("semanticClass") @Parameter(description = "semantic class") String semanticClassName) { - Locale locale = localeService.getLocale(language); + final Locale locale = localeService.getLocale(language); + final ZoneId zoneId = timeZoneProvider.getTimeZone(); Class semanticClass = semanticTagRegistry .getTagClassById(semanticClassName); @@ -886,7 +896,7 @@ public Response getSemanticItem(final @Context UriInfo uriInfo, final @Context H } EnrichedItemDTO dto = EnrichedItemDTOMapper.map(foundItem, false, null, uriBuilder(uriInfo, httpHeaders), - locale); + locale, zoneId); dto.editable = isEditable(dto.name); return JSONResponse.createResponse(Status.OK, dto, null); } @@ -935,8 +945,8 @@ private static Response getItemNotFoundResponse(String itemname) { * @return Response configured to represent the Item in depending on the status */ private Response getItemResponse(final @Nullable UriBuilder uriBuilder, Status status, @Nullable Item item, - Locale locale, @Nullable String errormessage) { - Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriBuilder, locale) : null; + Locale locale, ZoneId zoneId, @Nullable String errormessage) { + Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriBuilder, locale, zoneId) : null; return JSONResponse.createResponse(status, entity, errormessage); } diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java index 487083e0926..ae1adee3155 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapper.java @@ -12,6 +12,9 @@ */ package org.openhab.core.io.rest.core.item; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -29,7 +32,9 @@ import org.openhab.core.items.Item; import org.openhab.core.items.dto.ItemDTO; import org.openhab.core.items.dto.ItemDTOMapper; +import org.openhab.core.library.items.DateTimeItem; import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationService; @@ -51,6 +56,10 @@ public class EnrichedItemDTOMapper { private static final Pattern EXTRACT_TRANSFORM_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\):(.*)"); + private static final String DATE_FORMAT_PATTERN_WITH_TZ_RFC = "yyyy-MM-dd'T'HH:mm[:ss[.SSSSSSSSS]]Z"; + private static final DateTimeFormatter FORMATTER_TZ_RFC = DateTimeFormatter + .ofPattern(DATE_FORMAT_PATTERN_WITH_TZ_RFC); + private static final Logger LOGGER = LoggerFactory.getLogger(EnrichedItemDTOMapper.class); /** @@ -63,28 +72,39 @@ public class EnrichedItemDTOMapper { * @param uriBuilder if present the URI builder contains one template that will be replaced by the specific item * name * @param locale locale (can be null) + * @param zoneId time-zone id (can be null) * @return item DTO object */ public static EnrichedItemDTO map(Item item, boolean drillDown, @Nullable Predicate itemFilter, - @Nullable UriBuilder uriBuilder, @Nullable Locale locale) { + @Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId) { ItemDTO itemDTO = ItemDTOMapper.map(item); - return map(item, itemDTO, drillDown, itemFilter, uriBuilder, locale, new ArrayList<>()); + return map(item, itemDTO, drillDown, itemFilter, uriBuilder, locale, zoneId, new ArrayList<>()); } private static EnrichedItemDTO mapRecursive(Item item, @Nullable Predicate itemFilter, - @Nullable UriBuilder uriBuilder, @Nullable Locale locale, List parents) { + @Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId, List parents) { ItemDTO itemDTO = ItemDTOMapper.map(item); - return map(item, itemDTO, true, itemFilter, uriBuilder, locale, parents); + return map(item, itemDTO, true, itemFilter, uriBuilder, locale, zoneId, parents); } private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown, @Nullable Predicate itemFilter, @Nullable UriBuilder uriBuilder, @Nullable Locale locale, - List parents) { + @Nullable ZoneId zoneId, List parents) { if (item instanceof GroupItem) { // only add as parent item if it is a group, otherwise duplicate memberships trigger false warnings parents.add(item); } - String state = item.getState().toFullString(); + String state; + if (item instanceof DateTimeItem dateTimeItem && zoneId != null) { + DateTimeType dateTime = dateTimeItem.getStateAs(DateTimeType.class); + if (dateTime == null) { + state = item.getState().toFullString(); + } else { + state = formatDateTime(dateTime.getInstant(), zoneId); + } + } else { + state = item.getState().toFullString(); + } String transformedState = considerTransformation(item, locale); if (state.equals(transformedState)) { transformedState = null; @@ -117,7 +137,8 @@ private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown "Recursive group membership found: {} is a member of {}, but it is also one of its ancestors.", member.getName(), groupItem.getName()); } else if (itemFilter == null || itemFilter.test(member)) { - members.add(mapRecursive(member, itemFilter, uriBuilder, locale, new ArrayList<>(parents))); + members.add( + mapRecursive(member, itemFilter, uriBuilder, locale, zoneId, new ArrayList<>(parents))); } } memberDTOs = members.toArray(new EnrichedItemDTO[0]); @@ -134,6 +155,24 @@ private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, boolean drillDown return enrichedItemDTO; } + private static String formatDateTime(Instant instant, ZoneId zoneId) { + String formatted = instant.atZone(zoneId).format(FORMATTER_TZ_RFC); + if (formatted.contains(".")) { + String sign = ""; + if (formatted.contains("+")) { + sign = "+"; + } else if (formatted.contains("-")) { + sign = "-"; + } + if (!sign.isEmpty()) { + // the formatted string contains 9 fraction-of-second digits + // truncate at most 2 trailing groups of 000s + return formatted.replace("000" + sign, sign).replace("000" + sign, sign); + } + } + return formatted; + } + private static @Nullable StateDescription considerTransformation(@Nullable StateDescription stateDescription) { if (stateDescription != null) { String pattern = stateDescription.getPattern(); diff --git a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java index 85af584aa56..4fd1695c863 100644 --- a/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java +++ b/bundles/org.openhab.core.io.rest.core/src/test/java/org/openhab/core/io/rest/core/item/EnrichedItemDTOMapperTest.java @@ -55,31 +55,32 @@ public void testFiltering() { subGroup.addMember(stringItem); } - EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, null, null); + EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, null, null, + null); assertThat(dto.members.length, is(0)); - dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, null, null); + dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, null, null, null); assertThat(dto.members.length, is(3)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()), null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()), null, null, null); assertThat(dto.members.length, is(1)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, - i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null); + i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0)); dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, i -> CoreItemFactory.NUMBER.equals(i.getType()) || i.getType().equals(CoreItemFactory.STRING) || i instanceof GroupItem, - null, null); + null, null, null); assertThat(dto.members.length, is(2)); assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1)); } @@ -92,7 +93,7 @@ public void testDirectRecursiveMembershipDoesNotThrowStackOverflowException() { groupItem1.addMember(groupItem2); groupItem2.addMember(groupItem1); - assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null)); + assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null)); assertLogMessage(EnrichedItemDTOMapper.class, LogLevel.ERROR, "Recursive group membership found: group1 is a member of group2, but it is also one of its ancestors."); @@ -108,7 +109,7 @@ public void testIndirectRecursiveMembershipDoesNotThrowStackOverflowException() groupItem2.addMember(groupItem3); groupItem3.addMember(groupItem1); - assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null)); + assertDoesNotThrow(() -> EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null)); assertLogMessage(EnrichedItemDTOMapper.class, LogLevel.ERROR, "Recursive group membership found: group1 is a member of group3, but it is also one of its ancestors."); @@ -124,7 +125,7 @@ public void testDuplicateMembershipOfPlainItemsDoesNotTriggerWarning() { groupItem1.addMember(numberItem); groupItem2.addMember(numberItem); - EnrichedItemDTOMapper.map(groupItem1, true, null, null, null); + EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null); assertNoLogMessage(EnrichedItemDTOMapper.class); } @@ -139,7 +140,7 @@ public void testDuplicateMembershipOfGroupItemsDoesNotTriggerWarning() { groupItem1.addMember(groupItem3); groupItem2.addMember(groupItem3); - EnrichedItemDTOMapper.map(groupItem1, true, null, null, null); + EnrichedItemDTOMapper.map(groupItem1, true, null, null, null, null); assertNoLogMessage(EnrichedItemDTOMapper.class); } 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 1809c24de35..f9a02784959 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 @@ -16,7 +16,6 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -39,6 +38,7 @@ * @author Laurent Garnier - added methods toLocaleZone and toZone * @author Gaƫl L'hopital - added ability to use second and milliseconds unix time * @author Jimmy Tanagra - implement Comparable + * @author Jacob Laursen - Refactored to use {@link Instant} internally */ @NonNullByDefault public class DateTimeType implements PrimitiveType, State, Command, Comparable { @@ -73,48 +73,65 @@ public class DateTimeType implements PrimitiveType, State, Command, Comparable= 0 ? epoch : epoch * -1) + 1); - Instant i; // Assume that below 12 digits we're in seconds if (length < 12) { - i = Instant.ofEpochSecond(epoch); + instant = Instant.ofEpochSecond(epoch); } else { - i = Instant.ofEpochMilli(epoch); + instant = Instant.ofEpochMilli(epoch); } - date = ZonedDateTime.ofInstant(i, ZoneOffset.UTC); } catch (NumberFormatException notANumberException) { // date only if (zonedValue.length() == 10) { - date = parse(zonedValue + "T00:00:00"); + instant = parse(zonedValue + "T00:00:00"); } else { - date = parse(zonedValue.substring(0, 10) + "T00:00:00" + zonedValue.substring(10)); + instant = parse(zonedValue.substring(0, 10) + "T00:00:00" + zonedValue.substring(10)); } } } @@ -122,12 +139,10 @@ public DateTimeType(String zonedValue) { } catch (DateTimeParseException invalidFormatException) { throw new IllegalArgumentException(zonedValue + " is not in a valid format.", invalidFormatException); } - - zonedDateTime = date.withFixedOffsetZone(); } public ZonedDateTime getZonedDateTime() { - return zonedDateTime; + return instant.atZone(ZoneId.systemDefault()); } /** @@ -136,7 +151,7 @@ public ZonedDateTime getZonedDateTime() { * @return an {@link Instant} representation of the current object */ public Instant getInstant() { - return zonedDateTime.toInstant(); + return instant; } public static DateTimeType valueOf(String value) { @@ -146,49 +161,55 @@ public static DateTimeType valueOf(String value) { @Override public String format(@Nullable String pattern) { if (pattern == null) { - return DateTimeFormatter.ofPattern(DATE_PATTERN).format(zonedDateTime); + return DateTimeFormatter.ofPattern(DATE_PATTERN).format(instant.atZone(ZoneId.systemDefault())); } - return String.format(pattern, zonedDateTime); + return String.format(pattern, instant.atZone(ZoneId.systemDefault())); } public String format(Locale locale, String pattern) { - return String.format(locale, pattern, zonedDateTime); + return String.format(locale, pattern, instant.atZone(ZoneId.systemDefault())); } /** - * Create a {@link DateTimeType} being the translation of the current object to the locale time zone + * @deprecated + * Create a {@link DateTimeType} being the translation of the current object to the locale time zone * * @return a {@link DateTimeType} translated to the locale time zone * @throws DateTimeException if the converted zone ID has an invalid format or the result exceeds the supported date * range * @throws ZoneRulesException if the converted zone region ID cannot be found */ + @Deprecated public DateTimeType toLocaleZone() throws DateTimeException, ZoneRulesException { - return toZone(ZoneId.systemDefault()); + return new DateTimeType(instant); } /** - * Create a {@link DateTimeType} being the translation of the current object to a given zone + * @deprecated + * Create a {@link DateTimeType} being the translation of the current object to a given zone * * @param zone the target zone as a string * @return a {@link DateTimeType} translated to the given zone * @throws DateTimeException if the zone has an invalid format or the result exceeds the supported date range * @throws ZoneRulesException if the zone is a region ID that cannot be found */ + @Deprecated public DateTimeType toZone(String zone) throws DateTimeException, ZoneRulesException { - return toZone(ZoneId.of(zone)); + return new DateTimeType(instant); } /** - * Create a {@link DateTimeType} being the translation of the current object to a given zone + * @deprecated + * Create a {@link DateTimeType} being the translation of the current object to a given zone * * @param zoneId the target {@link ZoneId} * @return a {@link DateTimeType} translated to the given zone * @throws DateTimeException if the result exceeds the supported date range */ + @Deprecated public DateTimeType toZone(ZoneId zoneId) throws DateTimeException { - return new DateTimeType(zonedDateTime.withZoneSameInstant(zoneId)); + return new DateTimeType(instant); } @Override @@ -198,7 +219,7 @@ public String toString() { @Override public String toFullString() { - String formatted = zonedDateTime.format(FORMATTER_TZ_RFC); + String formatted = instant.atZone(ZoneId.systemDefault()).format(FORMATTER_TZ_RFC); if (formatted.contains(".")) { String sign = ""; if (formatted.contains("+")) { @@ -219,7 +240,7 @@ public String toFullString() { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + getZonedDateTime().hashCode(); + result = prime * result + instant.hashCode(); return result; } @@ -235,15 +256,15 @@ public boolean equals(@Nullable Object obj) { return false; } DateTimeType other = (DateTimeType) obj; - return zonedDateTime.compareTo(other.zonedDateTime) == 0; + return instant.compareTo(other.instant) == 0; } @Override public int compareTo(DateTimeType o) { - return zonedDateTime.compareTo(o.getZonedDateTime()); + return instant.compareTo(o.getInstant()); } - private ZonedDateTime parse(String value) throws DateTimeParseException { + private Instant parse(String value) throws DateTimeParseException { ZonedDateTime date; try { date = ZonedDateTime.parse(value, PARSER_TZ_RFC); @@ -260,6 +281,6 @@ private ZonedDateTime parse(String value) throws DateTimeParseException { } } - return date; + return date.toInstant(); } } diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java index 9b42d301cd5..2da6fac85ee 100644 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/library/types/DateTimeTypeTest.java @@ -183,19 +183,19 @@ public static Collection parameters() { { new ParameterSet(TimeZone.getTimeZone("UTC"), initTimeMap(), TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47.033+0000", "2014-03-30T10:58:47.033+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), initTimeMap(), TimeZone.getTimeZone("CET"), - "2014-03-30T10:58:47.033+0200", "2014-03-30T08:58:47.033+0000") }, + "2014-03-30T08:58:47.033+0000", "2014-03-30T08:58:47.033+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47.23", "2014-03-30T10:58:47.230+0000", "2014-03-30T10:58:47.230+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "2014-03-30T10:58:47UTC", "2014-03-30T10:58:47.000+0000", "2014-03-30T10:58:47.000+0000") }, { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("UTC"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("CET"), "2014-03-30T10:58:47.033+0200", "2014-03-30T10:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("CET"), "2014-03-30T10:58:47CET", "2014-03-30T10:58:47.000+0200", "2014-03-30T10:58:47.000+0200") }, { new ParameterSet(TimeZone.getTimeZone("GMT+5"), "2014-03-30T10:58:47.000Z", - "2014-03-30T10:58:47.000+0000", "2014-03-30T15:58:47.000+0500") }, + "2014-03-30T15:58:47.000+0500", "2014-03-30T15:58:47.000+0500") }, { new ParameterSet(TimeZone.getTimeZone("GMT+2"), null, null, "2014-03-30T10:58:47", "2014-03-30T10:58:47.000+0200", "2014-03-30T10:58:47.000+0200", null, "%1$td.%1$tm.%1$tY %1$tH:%1$tM", "30.03.2014 10:58") }, @@ -203,15 +203,15 @@ public static Collection parameters() { "2014-03-30T10:58:47.033+0000", "2014-03-30T10:58:47.033+0000") }, // Parameter set with an invalid time zone id as input, leading to GMT being considered { new ParameterSet(TimeZone.getTimeZone("CET"), initTimeMap(), TimeZone.getTimeZone("+02:00"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, // Parameter set with an invalid time zone id as input, leading to GMT being considered { new ParameterSet(TimeZone.getTimeZone("GMT+2"), initTimeMap(), TimeZone.getTimeZone("GML"), - "2014-03-30T10:58:47.033+0000", "2014-03-30T12:58:47.033+0200") }, + "2014-03-30T12:58:47.033+0200", "2014-03-30T12:58:47.033+0200") }, { new ParameterSet(TimeZone.getTimeZone("GMT-2"), initTimeMap(), TimeZone.getTimeZone("GMT+3"), null, - "2014-03-30T10:58:47.033+0300", "2014-03-30T05:58:47.033-0200", Locale.GERMAN, - "%1$tA %1$td.%1$tm.%1$tY %1$tH:%1$tM", "Sonntag 30.03.2014 10:58") }, + "2014-03-30T05:58:47.033-0200", "2014-03-30T05:58:47.033-0200", Locale.GERMAN, + "%1$tA %1$td.%1$tm.%1$tY %1$tH:%1$tM", "Sonntag 30.03.2014 05:58") }, { new ParameterSet(TimeZone.getTimeZone("GMT-2"), initTimeMap(), TimeZone.getTimeZone("GMT-4"), - "2014-03-30T10:58:47.033-0400", "2014-03-30T12:58:47.033-0200") }, + "2014-03-30T12:58:47.033-0200", "2014-03-30T12:58:47.033-0200") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "10:58:47", "1970-01-01T10:58:47.000+0000", "1970-01-01T10:58:47.000+0000") }, { new ParameterSet(TimeZone.getTimeZone("UTC"), "10:58", "1970-01-01T10:58:00.000+0000", @@ -295,16 +295,21 @@ public void zonedParsingTest() { @Test public void instantParsingTest() { - DateTimeType dt1 = new DateTimeType("2019-06-12T17:30:00Z"); - DateTimeType dt2 = new DateTimeType("2019-06-12T17:30:00+0000"); - DateTimeType dt3 = new DateTimeType("2019-06-12T19:30:00+0200"); + DateTimeType dt1 = new DateTimeType(Instant.parse("2019-06-12T17:30:00Z")); + DateTimeType dt2 = new DateTimeType("2019-06-12T17:30:00Z"); + DateTimeType dt3 = new DateTimeType("2019-06-12T17:30:00+0000"); + DateTimeType dt4 = new DateTimeType("2019-06-12T19:30:00+0200"); assertThat(dt1, is(dt2)); + assertThat(dt2, is(dt3)); + assertThat(dt3, is(dt4)); Instant i1 = dt1.getInstant(); Instant i2 = dt2.getInstant(); Instant i3 = dt3.getInstant(); + Instant i4 = dt4.getInstant(); assertThat(i1, is(i2)); - assertThat(i1, is(i3)); + assertThat(i2, is(i3)); + assertThat(i3, is(i4)); } @Test @@ -369,25 +374,6 @@ public void formattingTest(ParameterSet parameterSet) { } } - @ParameterizedTest - @MethodSource("parameters") - public void changingZoneTest(ParameterSet parameterSet) { - TimeZone.setDefault(parameterSet.defaultTimeZone); - DateTimeType dt = createDateTimeType(parameterSet); - DateTimeType dt2 = dt.toLocaleZone(); - assertEquals(parameterSet.expectedResultLocalTZ, dt2.toFullString()); - dt2 = dt.toZone(parameterSet.defaultTimeZone.toZoneId()); - assertEquals(parameterSet.expectedResultLocalTZ, dt2.toFullString()); - } - - @ParameterizedTest - @MethodSource("parameters") - public void changingZoneThrowsExceptionTest(ParameterSet parameterSet) { - TimeZone.setDefault(parameterSet.defaultTimeZone); - DateTimeType dt = createDateTimeType(parameterSet); - assertThrows(DateTimeException.class, () -> dt.toZone("XXX")); - } - private DateTimeType createDateTimeType(ParameterSet parameterSet) throws DateTimeException { Map inputTimeMap = parameterSet.inputTimeMap; TimeZone inputTimeZone = parameterSet.inputTimeZone;