Skip to content

Commit

Permalink
[core] Switch DateTimeType to Instant internally for consistent t…
Browse files Browse the repository at this point in the history
…ime-zone handling (#3583)

Signed-off-by: Jacob Laursen <[email protected]>
  • Loading branch information
jlaur authored Dec 8, 2024
1 parent a35041a commit b31ff66
Show file tree
Hide file tree
Showing 26 changed files with 337 additions and 252 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.annotation.security.RolesAllowed;
Expand Down Expand Up @@ -76,6 +77,7 @@
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.events.Event;
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.RESTConstants;
Expand Down Expand Up @@ -133,6 +135,7 @@ public class RuleResource implements RESTResource {
private final RuleManager ruleManager;
private final RuleRegistry ruleRegistry;
private final ManagedRuleProvider managedRuleProvider;
private final TimeZoneProvider timeZoneProvider;
private final RegistryChangedRunnableListener<Rule> resetLastModifiedChangeListener = new RegistryChangedRunnableListener<>(
() -> lastModified = null);

Expand All @@ -144,11 +147,13 @@ public RuleResource( //
final @Reference DTOMapper dtoMapper, //
final @Reference RuleManager ruleManager, //
final @Reference RuleRegistry ruleRegistry, //
final @Reference ManagedRuleProvider managedRuleProvider) {
final @Reference ManagedRuleProvider managedRuleProvider, //
final @Reference TimeZoneProvider timeZoneProvider) {
this.dtoMapper = dtoMapper;
this.ruleManager = ruleManager;
this.ruleRegistry = ruleRegistry;
this.managedRuleProvider = managedRuleProvider;
this.timeZoneProvider = timeZoneProvider;

this.ruleRegistry.addRegistryChangeListener(resetLastModifiedChangeListener);
}
Expand Down Expand Up @@ -419,10 +424,10 @@ public Response simulateRules(
+ DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]") @QueryParam("from") @Nullable String from,
@Parameter(description = "End time of the simulated rule executions. Will default to 30 days after the start time. Must be less than 180 days after the given start time. ["
+ DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS + "]") @QueryParam("until") @Nullable String until) {
final ZonedDateTime fromDate = from == null || from.isEmpty() ? ZonedDateTime.now() : parseTime(from);
final ZonedDateTime untilDate = until == null || until.isEmpty() ? fromDate.plusDays(31) : parseTime(until);
final ZonedDateTime fromDate = parseTime(from, ZonedDateTime::now);
final ZonedDateTime untilDate = parseTime(until, () -> fromDate.plusDays(31));

if (daysBetween(fromDate, untilDate) >= 180) {
if (ChronoUnit.DAYS.between(fromDate, untilDate) >= 180) {
return JSONResponse.createErrorResponse(Status.BAD_REQUEST,
"Simulated time span must be smaller than 180 days.");
}
Expand All @@ -431,13 +436,12 @@ public Response simulateRules(
return Response.ok(ruleExecutions.toList()).build();
}

private static ZonedDateTime parseTime(String sTime) {
private ZonedDateTime parseTime(@Nullable String sTime, Supplier<ZonedDateTime> defaultSupplier) {
if (sTime == null || sTime.isEmpty()) {
return defaultSupplier.get();
}
final DateTimeType dateTime = new DateTimeType(sTime);
return dateTime.getZonedDateTime();
}

private static long daysBetween(ZonedDateTime d1, ZonedDateTime d2) {
return ChronoUnit.DAYS.between(d1, d2);
return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone());
}

@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ private void process(Type value) {
cronExpression = CronAdjuster.REBOOT;
} else if (value instanceof DateTimeType dateTimeType) {
boolean itemIsTimeOnly = dateTimeType.toString().startsWith("1970-01-01T");
cronExpression = dateTimeType.getZonedDateTime().withZoneSameInstant(ZoneId.systemDefault())
.plusSeconds(offset.longValue())
cronExpression = dateTimeType.getZonedDateTime(ZoneId.systemDefault()).plusSeconds(offset.longValue())
.format(timeOnly || itemIsTimeOnly ? CRON_TIMEONLY_FORMATTER : CRON_FORMATTER);
startScheduler();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.core.automation.internal.module.handler;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
Expand Down Expand Up @@ -158,9 +159,9 @@ private boolean lessThanOrEqualsToItemState(String itemName, String state) throw
Item item = itemRegistry.getItem(itemName);
State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state);
State itemState = item.getState();
if (itemState instanceof DateTimeType type) {
ZonedDateTime itemTime = type.getZonedDateTime();
ZonedDateTime compareTime = getCompareTime(state);
if (itemState instanceof DateTimeType dateTimeState) {
Instant itemTime = dateTimeState.getInstant();
Instant compareTime = getCompareTime(state);
return itemTime.compareTo(compareTime) <= 0;
} else if (itemState instanceof QuantityType qtState) {
if (compareState instanceof DecimalType type) {
Expand Down Expand Up @@ -195,9 +196,9 @@ private boolean greaterThanOrEqualsToItemState(String itemName, String state) th
Item item = itemRegistry.getItem(itemName);
State compareState = TypeParser.parseState(item.getAcceptedDataTypes(), state);
State itemState = item.getState();
if (itemState instanceof DateTimeType type) {
ZonedDateTime itemTime = type.getZonedDateTime();
ZonedDateTime compareTime = getCompareTime(state);
if (itemState instanceof DateTimeType dateTimeState) {
Instant itemTime = dateTimeState.getInstant();
Instant compareTime = getCompareTime(state);
return itemTime.compareTo(compareTime) >= 0;
} else if (itemState instanceof QuantityType qtState) {
if (compareState instanceof DecimalType type) {
Expand Down Expand Up @@ -252,36 +253,36 @@ public void dispose() {
eventSubscriberRegistration.unregister();
}

private ZonedDateTime getCompareTime(String input) {
private Instant getCompareTime(String input) {
if (input.isBlank()) {
// no parameter given, use now
return ZonedDateTime.now();
return Instant.now();
}
try {
return ZonedDateTime.parse(input);
return ZonedDateTime.parse(input).toInstant();
} catch (DateTimeParseException ignored) {
}
try {
return LocalDateTime.parse(input, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.atZone(timeZoneProvider.getTimeZone());
.atZone(timeZoneProvider.getTimeZone()).toInstant();
} catch (DateTimeParseException ignored) {
}
try {
int dayPosition = input.indexOf("D");
if (dayPosition == -1) {
// no date in string, add period symbol and time separator
return ZonedDateTime.now().plus(Duration.parse("PT" + input));
return Instant.now().plus(Duration.parse("PT" + input));
} else if (dayPosition == input.length() - 1) {
// day is the last symbol, only add the period symbol
return ZonedDateTime.now().plus(Duration.parse("P" + input));
return Instant.now().plus(Duration.parse("P" + input));
} else {
// add period symbol and time separator
return ZonedDateTime.now().plus(Duration
return Instant.now().plus(Duration
.parse("P" + input.substring(0, dayPosition + 1) + "T" + input.substring(dayPosition + 1)));
}
} catch (DateTimeParseException e) {
logger.warn("Couldn't get a comparable time from '{}', using now", input);
}
return ZonedDateTime.now();
return Instant.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<Item> resetLastModifiedItemChangeListener = new RegistryChangedRunnableListener<>(
() -> lastModified = null);
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<String> namespaces = splitAndFilterNamespaces(namespaceSelector, locale);

final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders);
Expand All @@ -256,7 +262,7 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead
}

Stream<EnrichedItemDTO> 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,
Expand All @@ -267,7 +273,7 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead
}

Stream<EnrichedItemDTO> 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 -> {
Expand Down Expand Up @@ -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<String> namespaces = splitAndFilterNamespaces(namespaceSelector, locale);

// get item
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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<? extends org.openhab.core.semantics.Tag> semanticClass = semanticTagRegistry
.getTagClassById(semanticClassName);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public Response httpPutPersistenceItemData(@Context HttpHeaders headers,

private ZonedDateTime convertTime(String sTime) {
DateTimeType dateTime = new DateTimeType(sTime);
return dateTime.getZonedDateTime();
return dateTime.getZonedDateTime(timeZoneProvider.getTimeZone());
}

private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, @Nullable String timeBegin,
Expand Down
Loading

0 comments on commit b31ff66

Please sign in to comment.