Item REST endpoint include item parents (#4861)

* parents in item DTO

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege
2025-07-12 23:48:26 +02:00
committed by GitHub
parent d83d35cd00
commit 69bb634ba6
4 changed files with 58 additions and 16 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
@@ -142,6 +142,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* @author Stefan Triller - Added bulk item add method
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
* @author Wouter Born - Migrated to OpenAPI annotations
* @author Mark Herwege - Added option to retrieve item groups with item REST call
*/
@Component
@JaxrsResource
@@ -253,6 +254,7 @@ public class ItemResource implements RESTResource {
@QueryParam("tags") @Parameter(description = "item tag filter") @Nullable String tags,
@DefaultValue(".*") @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (returns all if no value given)") @Nullable String namespaceSelector,
@DefaultValue("false") @QueryParam("recursive") @Parameter(description = "get member items recursively") boolean recursive,
@DefaultValue("false") @QueryParam("parents") @Parameter(description = "get parent group items recursively") boolean parents,
@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);
@@ -285,7 +287,11 @@ public class ItemResource implements RESTResource {
Stream<EnrichedItemDTO> itemStream = getItems(type, tags).stream() //
.map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale, zoneId)) //
.peek(dto -> addMetadata(dto, namespaces, null)) //
.peek(dto -> {
if (parents) {
addParents(dto, uriInfo, httpHeaders, locale, zoneId);
}
}).peek(dto -> addMetadata(dto, namespaces, null)) //
.peek(dto -> dto.editable = isEditable(dto)) //
.peek(dto -> {
if (dto instanceof EnrichedGroupItemDTO enrichedGroupItemDTO) {
@@ -293,7 +299,13 @@ public class ItemResource implements RESTResource {
member.editable = isEditable(member);
}
}
if (dto.parents != null) {
for (EnrichedItemDTO parent : dto.parents) {
parent.editable = isEditable(parent);
}
}
});
itemStream = dtoMapper.limitToFields(itemStream, fields);
return Response.ok(new Stream2JSONInputStream(itemStream)).build();
}
@@ -333,6 +345,7 @@ public class ItemResource implements RESTResource {
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language,
@DefaultValue(".*") @QueryParam("metadata") @Parameter(description = "metadata selector - a comma separated list or a regular expression (returns all if no value given)") @Nullable String namespaceSelector,
@DefaultValue("true") @QueryParam("recursive") @Parameter(description = "get member items if the item is a group item") boolean recursive,
@DefaultValue("false") @QueryParam("parents") @Parameter(description = "get parent group items recursively") boolean parents,
@PathParam("itemname") @Parameter(description = "item name") String itemname) {
final Locale locale = localeService.getLocale(language);
final ZoneId zoneId = timeZoneProvider.getTimeZone();
@@ -345,6 +358,9 @@ public class ItemResource implements RESTResource {
if (item != null) {
EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder(uriInfo, httpHeaders),
locale, zoneId);
if (parents) {
addParents(dto, uriInfo, httpHeaders, locale, zoneId);
}
addMetadata(dto, namespaces, null);
dto.editable = isEditable(dto);
if (dto instanceof EnrichedGroupItemDTO enrichedGroupItemDTO) {
@@ -352,6 +368,11 @@ public class ItemResource implements RESTResource {
member.editable = isEditable(member);
}
}
if (dto.parents != null) {
for (EnrichedItemDTO parent : dto.parents) {
parent.editable = isEditable(parent);
}
}
return JSONResponse.createResponse(Status.OK, dto, null);
} else {
return getItemNotFoundResponse(itemname);
@@ -1054,12 +1075,27 @@ public class ItemResource implements RESTResource {
addMetadata(member, namespaces, filter);
}
}
if (dto.parents != null) {
for (EnrichedItemDTO parent : dto.parents) {
addMetadata(parent, namespaces, filter);
}
}
if (!metadata.isEmpty()) {
// we only set it in the dto if there is really data available
dto.metadata = metadata;
}
}
private void addParents(EnrichedItemDTO dto, UriInfo uriInfo, HttpHeaders httpHeaders, Locale locale,
ZoneId zoneId) {
dto.parents = dto.groupNames.stream() //
.map(groupName -> getItem(groupName)).filter(Objects::nonNull) //
.map(parentItem -> EnrichedItemDTOMapper.map(parentItem, false, null, uriBuilder(uriInfo, httpHeaders),
locale, zoneId)) //
.peek(parentEnrichedItemDto -> addParents(parentEnrichedItemDto, uriInfo, httpHeaders, locale, zoneId)) //
.toArray(size -> new EnrichedItemDTO[size]);
}
private boolean isEditable(EnrichedItemDTO item) {
return isItemEditable(item.name);
}
@@ -25,6 +25,7 @@ import org.openhab.core.types.StateDescription;
* @author Dennis Nobel - Initial contribution
* @author Kai Kreuzer - Added metadata
* @author Mark Herwege - Added default unit symbol
* @author Mark Herwege - Added parent groups
*/
public class EnrichedItemDTO extends ItemDTO {
@@ -38,6 +39,7 @@ public class EnrichedItemDTO extends ItemDTO {
public Long lastStateChange;
public String unitSymbol;
public Map<String, Object> metadata;
public EnrichedItemDTO[] parents = null;
public Boolean editable;
public EnrichedItemDTO(ItemDTO itemDTO, String link, String state, String lastState, Long lastStateUpdate,
@@ -132,7 +132,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
item4.setLabel(ITEM_LABEL4);
Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, null, null, false,
null, false);
false, null, false);
assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4));
}
@@ -140,7 +140,8 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
public void shouldReturnUnicodeItem() throws IOException, TransformationException {
item4.setLabel(ITEM_LABEL4);
Response response = itemResource.getItemByName(uriInfoMock, httpHeadersMock, null, null, true, ITEM_NAME4);
Response response = itemResource.getItemByName(uriInfoMock, httpHeadersMock, null, null, true, false,
ITEM_NAME4);
assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4));
}
@@ -153,30 +154,30 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
item4.addTag("Tag4");
Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag1", null,
false, null, false);
false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2));
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag2", null, false, null,
false);
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "Tag2", null, false, false,
null, false);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME2, ITEM_NAME3));
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "NotExistingTag", null,
false, null, false);
false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@Test
public void shouldFilterItemsByType() throws Exception {
Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.SWITCH,
null, null, false, null, false);
null, null, false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2));
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.DIMMER, null,
null, false, null, false);
null, false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME3));
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, CoreItemFactory.COLOR, null, null,
false, null, false);
false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@@ -185,17 +186,17 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
managedItemProvider.add(new SwitchItem("Switch"));
Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null,
false, null, false);
false, false, null, false);
assertThat(readItemNamesFromResponse(response), hasSize(0));
itemResource.addTag("Switch", "MyTag");
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null,
false);
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, false,
null, false);
assertThat(readItemNamesFromResponse(response), hasSize(1));
itemResource.removeTag("Switch", "MyTag");
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, null,
false);
response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null, false, false,
null, false);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@@ -204,7 +205,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
managedItemProvider.add(new SwitchItem("Switch"));
itemResource.addTag("Switch", "MyTag");
Response response = itemResource.getItems(uriInfoMock, httpHeadersMock, request, null, null, "MyTag", null,
false, "type,name", false);
false, false, "type,name", false);
JsonElement result = JsonParser.parseString(toString(response.getEntity()));
JsonElement expected = JsonParser.parseString("[{type: \"Switch\", name: \"Switch\"}]");