Skip to content

Commit

Permalink
Use Instant internally
Browse files Browse the repository at this point in the history
Signed-off-by: Jacob Laursen <[email protected]>
  • Loading branch information
jlaur committed Nov 8, 2024
1 parent 591d8d9 commit 7df4b06
Show file tree
Hide file tree
Showing 15 changed files with 201 additions and 147 deletions.
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 @@ -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;
Expand All @@ -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;
Expand All @@ -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);

/**
Expand All @@ -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<Item> 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<Item> itemFilter,
@Nullable UriBuilder uriBuilder, @Nullable Locale locale, List<Item> parents) {
@Nullable UriBuilder uriBuilder, @Nullable Locale locale, @Nullable ZoneId zoneId, List<Item> 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<Item> itemFilter, @Nullable UriBuilder uriBuilder, @Nullable Locale locale,
List<Item> parents) {
@Nullable ZoneId zoneId, List<Item> 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;
Expand Down Expand Up @@ -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]);
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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.");
Expand All @@ -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.");
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
Loading

0 comments on commit 7df4b06

Please sign in to comment.