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 6, 2024
1 parent 591d8d9 commit 20bc1e3
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 92 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 20bc1e3

Please sign in to comment.