Add REST support for deleting links and removing orphaned links (#2970)

* add link remove for item to registry

Signed-off-by: Jan N. Klug <github@klug.nrw>
pull/3053/head
J-N-K 2022-07-19 09:24:47 +02:00 committed by GitHub
parent b6acaf7887
commit ae3d7c749c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 312 additions and 6 deletions

View File

@ -16,6 +16,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -614,6 +615,21 @@ public class ItemResource implements RESTResource {
return Response.ok(null, MediaType.TEXT_PLAIN).build(); return Response.ok(null, MediaType.TEXT_PLAIN).build();
} }
@POST
@RolesAllowed({ Role.ADMIN })
@Path("/metadata/purge")
@Operation(operationId = "purgeDatabase", summary = "Remove unused/orphaned metadata.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK") })
public Response purge() {
Collection<String> itemNames = itemRegistry.stream().map(Item::getName)
.collect(Collectors.toCollection(HashSet::new));
metadataRegistry.getAll().stream().filter(md -> !itemNames.contains(md.getUID().getItemName()))
.forEach(md -> metadataRegistry.remove(md.getUID()));
return Response.ok().build();
}
/** /**
* Create or Update an item by supplying an item bean. * Create or Update an item by supplying an item bean.
* *

View File

@ -13,6 +13,7 @@
package org.openhab.core.io.rest.core.internal.link; package org.openhab.core.io.rest.core.internal.link;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -20,6 +21,7 @@ import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
@ -47,6 +49,7 @@ import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.link.AbstractLink; import org.openhab.core.thing.link.AbstractLink;
import org.openhab.core.thing.link.ItemChannelLink; import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ItemChannelLinkRegistry; import org.openhab.core.thing.link.ItemChannelLinkRegistry;
@ -141,6 +144,24 @@ public class ItemChannelLinkResource implements RESTResource {
return Response.ok(new Stream2JSONInputStream(linkStream)).build(); return Response.ok(new Stream2JSONInputStream(linkStream)).build();
} }
@DELETE
@RolesAllowed({ Role.ADMIN })
@Path("/{object}")
@Operation(operationId = "removeAllLinksForObject", summary = "Delete all links that refer to an item or thing.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK") })
public Response removeAllLinksForObject(
@PathParam("object") @Parameter(description = "item name or thing UID") String object) {
int removedLinks;
try {
ThingUID thingUID = new ThingUID(object);
removedLinks = itemChannelLinkRegistry.removeLinksForThing(thingUID);
} catch (IllegalArgumentException e) {
removedLinks = itemChannelLinkRegistry.removeLinksForItem(object);
}
return Response.ok(Map.of("count", removedLinks)).build();
}
@GET @GET
@Path("/{itemName}/{channelUID}") @Path("/{itemName}/{channelUID}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -273,6 +294,17 @@ public class ItemChannelLinkResource implements RESTResource {
return Response.ok(null, MediaType.TEXT_PLAIN).build(); return Response.ok(null, MediaType.TEXT_PLAIN).build();
} }
@POST
@RolesAllowed({ Role.ADMIN })
@Path("/purge")
@Operation(operationId = "purgeDatabase", summary = "Remove unused/orphaned links.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK") })
public Response purge() {
itemChannelLinkRegistry.purge();
return Response.ok().build();
}
private boolean isEditable(String linkId) { private boolean isEditable(String linkId) {
return managedItemChannelLinkProvider.get(linkId) != null; return managedItemChannelLinkProvider.get(linkId) != null;
} }

View File

@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.core.internal.link;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
import javax.ws.rs.core.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
import org.openhab.core.thing.profiles.ProfileTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeRegistry;
/**
* The {@link ItemChannelLinkResourceTest} tests the {@link ItemChannelLinkResource}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class ItemChannelLinkResourceTest {
private static final int EXPECTED_REMOVED_LINKS = 5;
private @Mock @NonNullByDefault({}) ItemRegistry itemRegistryMock;
private @Mock @NonNullByDefault({}) ThingRegistry thingRegistryMock;
private @Mock @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistryMock;
private @Mock @NonNullByDefault({}) ProfileTypeRegistry profileTypeRegistryMock;
private @Mock @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistryMock;
private @Mock @NonNullByDefault({}) ManagedItemChannelLinkProvider managedItemChannelLinkProviderMock;
private @NonNullByDefault({}) ItemChannelLinkResource itemChannelLinkResource;
@BeforeEach
public void setup() {
itemChannelLinkResource = new ItemChannelLinkResource(itemRegistryMock, thingRegistryMock,
channelTypeRegistryMock, profileTypeRegistryMock, itemChannelLinkRegistryMock,
managedItemChannelLinkProviderMock);
when(itemChannelLinkRegistryMock.removeLinksForItem(any())).thenReturn(EXPECTED_REMOVED_LINKS);
when(itemChannelLinkRegistryMock.removeLinksForThing(any())).thenReturn(EXPECTED_REMOVED_LINKS);
}
@Test
public void testRemoveAllLinksForItem() {
try (Response response = itemChannelLinkResource.removeAllLinksForObject("testItem")) {
assertThat(response.getStatus(), is(200));
Object responseEntity = response.getEntity();
assertThat(responseEntity, instanceOf(Map.class));
assertThat(((Map<?, ?>) responseEntity).get("count"), is(EXPECTED_REMOVED_LINKS));
}
verify(itemChannelLinkRegistryMock).removeLinksForItem(eq("testItem"));
}
@Test
public void testRemoveAllLinksForThing() {
try (Response response = itemChannelLinkResource.removeAllLinksForObject("binding:type:thing")) {
assertThat(response.getStatus(), is(200));
Object responseEntity = response.getEntity();
assertThat(responseEntity, instanceOf(Map.class));
assertThat(((Map<?, ?>) responseEntity).get("count"), is(EXPECTED_REMOVED_LINKS));
}
verify(itemChannelLinkRegistryMock).removeLinksForThing(eq(new ThingUID("binding:type:thing")));
}
}

View File

@ -12,16 +12,19 @@
*/ */
package org.openhab.core.thing.link; package org.openhab.core.thing.link;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.ManagedProvider;
import org.openhab.core.events.EventPublisher; import org.openhab.core.events.EventPublisher;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry; import org.openhab.core.items.ItemRegistry;
import org.openhab.core.service.ReadyService; import org.openhab.core.service.ReadyService;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry; import org.openhab.core.thing.ThingRegistry;
@ -127,10 +130,50 @@ public class ItemChannelLinkRegistry extends AbstractLinkRegistry<ItemChannelLin
super.unsetEventPublisher(eventPublisher); super.unsetEventPublisher(eventPublisher);
} }
public void removeLinksForThing(final ThingUID thingUID) { /**
((ManagedItemChannelLinkProvider) getManagedProvider() * Remove all links related to a thing
.orElseThrow(() -> new IllegalStateException("ManagedProvider is not available"))) *
.removeLinksForThing(thingUID); * @param thingUID the UID of the thing
* @return the number of removed links
*/
public int removeLinksForThing(final ThingUID thingUID) {
ManagedItemChannelLinkProvider managedProvider = (ManagedItemChannelLinkProvider) getManagedProvider()
.orElseThrow(() -> new IllegalStateException("ManagedProvider is not available"));
return managedProvider.removeLinksForThing(thingUID);
}
/**
* Remove all links related to an item
*
* @param itemName the name of the item
* @return the number of removed links
*/
public int removeLinksForItem(final String itemName) {
ManagedItemChannelLinkProvider managedProvider = (ManagedItemChannelLinkProvider) getManagedProvider()
.orElseThrow(() -> new IllegalStateException("ManagedProvider is not available"));
return managedProvider.removeLinksForItem(itemName);
}
/**
* Remove all orphaned (item or channel missing) links
*
* @return the number of removed links
*/
public int purge() {
ManagedProvider<ItemChannelLink, String> managedProvider = getManagedProvider()
.orElseThrow(() -> new IllegalStateException("ManagedProvider is not available"));
Set<String> allItems = itemRegistry.stream().map(Item::getName).collect(Collectors.toSet());
Set<ChannelUID> allChannels = thingRegistry.stream().map(Thing::getChannels).flatMap(List::stream)
.map(Channel::getUID).collect(Collectors.toSet());
Set<String> toRemove = managedProvider.getAll().stream()
.filter(link -> !allItems.contains(link.getItemName()) || !allChannels.contains(link.getLinkedUID()))
.map(ItemChannelLink::getUID).collect(Collectors.toSet());
toRemove.forEach(managedProvider::remove);
return toRemove.size();
} }
@Override @Override

View File

@ -48,12 +48,27 @@ public class ManagedItemChannelLinkProvider extends DefaultAbstractManagedProvid
return key; return key;
} }
public void removeLinksForThing(ThingUID thingUID) { public int removeLinksForThing(ThingUID thingUID) {
int removedLinks = 0;
Collection<ItemChannelLink> itemChannelLinks = getAll(); Collection<ItemChannelLink> itemChannelLinks = getAll();
for (ItemChannelLink itemChannelLink : itemChannelLinks) { for (ItemChannelLink itemChannelLink : itemChannelLinks) {
if (itemChannelLink.getLinkedUID().getThingUID().equals(thingUID)) { if (itemChannelLink.getLinkedUID().getThingUID().equals(thingUID)) {
this.remove(itemChannelLink.getUID()); this.remove(itemChannelLink.getUID());
removedLinks++;
} }
} }
return removedLinks;
}
public int removeLinksForItem(String itemName) {
int removedLinks = 0;
Collection<ItemChannelLink> itemChannelLinks = getAll();
for (ItemChannelLink itemChannelLink : itemChannelLinks) {
if (itemChannelLink.getItemName().equals(itemName)) {
this.remove(itemChannelLink.getUID());
removedLinks++;
}
}
return removedLinks;
} }
} }

View File

@ -12,17 +12,27 @@
*/ */
package org.openhab.core.thing.link; package org.openhab.core.thing.link;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.core.items.ManagedItemProvider;
import org.openhab.core.library.CoreItemFactory; import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.test.java.JavaOSGiTest; import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ManagedThingProvider; import org.openhab.core.thing.ManagedThingProvider;
@ -42,6 +52,12 @@ import org.osgi.service.component.ComponentContext;
@NonNullByDefault @NonNullByDefault
public class ItemChannelLinkOSGiTest extends JavaOSGiTest { public class ItemChannelLinkOSGiTest extends JavaOSGiTest {
private static final String BULK_BASE_THING_UID = "binding:type:thing";
private static final String BULK_BASE_ITEM_NAME = "item";
private static int BULK_ITEM_COUNT = 3;
private static int BULK_THING_COUNT = 3;
private static int BULK_CHANNEL_COUNT = 3;
private static final String ITEM = "item"; private static final String ITEM = "item";
private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("binding:thing"); private static final ThingTypeUID THING_TYPE_UID = new ThingTypeUID("binding:thing");
private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thing"); private static final ThingUID THING_UID = new ThingUID(THING_TYPE_UID, "thing");
@ -51,6 +67,7 @@ public class ItemChannelLinkOSGiTest extends JavaOSGiTest {
private @NonNullByDefault({}) ManagedItemChannelLinkProvider managedItemChannelLinkProvider; private @NonNullByDefault({}) ManagedItemChannelLinkProvider managedItemChannelLinkProvider;
private @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistry; private @NonNullByDefault({}) ItemChannelLinkRegistry itemChannelLinkRegistry;
private @NonNullByDefault({}) ManagedThingProvider managedThingProvider; private @NonNullByDefault({}) ManagedThingProvider managedThingProvider;
private @NonNullByDefault({}) ManagedItemProvider managedItemProvider;
@BeforeEach @BeforeEach
public void setup() { public void setup() {
@ -58,6 +75,8 @@ public class ItemChannelLinkOSGiTest extends JavaOSGiTest {
managedThingProvider = getService(ManagedThingProvider.class); managedThingProvider = getService(ManagedThingProvider.class);
managedThingProvider.add(ThingBuilder.create(THING_TYPE_UID, THING_UID) managedThingProvider.add(ThingBuilder.create(THING_TYPE_UID, THING_UID)
.withChannel(ChannelBuilder.create(CHANNEL_UID, CoreItemFactory.COLOR).build()).build()); .withChannel(ChannelBuilder.create(CHANNEL_UID, CoreItemFactory.COLOR).build()).build());
managedItemProvider = getService(ManagedItemProvider.class);
itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class); itemChannelLinkRegistry = getService(ItemChannelLinkRegistry.class);
managedItemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class); managedItemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
assertNotNull(managedItemChannelLinkProvider); assertNotNull(managedItemChannelLinkProvider);
@ -125,8 +144,94 @@ public class ItemChannelLinkOSGiTest extends JavaOSGiTest {
} }
@Test @Test
public void assertThatgetBoundThingsReturnsEmptySet() { public void assertThatGetBoundThingsReturnsEmptySet() {
Set<Thing> boundThings = itemChannelLinkRegistry.getBoundThings("notExistingItem"); Set<Thing> boundThings = itemChannelLinkRegistry.getBoundThings("notExistingItem");
assertTrue(boundThings.isEmpty()); assertTrue(boundThings.isEmpty());
} }
@Test
public void assertThatAllLinksForItemCanBeDeleted() {
fillRegistryForBulkTests();
String itemToRemove = BULK_BASE_ITEM_NAME + "_0_1_1";
int removed = itemChannelLinkRegistry.removeLinksForItem(itemToRemove);
assertThat(removed, is(1));
assertThat(itemChannelLinkRegistry.stream().map(ItemChannelLink::getItemName).collect(Collectors.toList()),
not(hasItem(itemToRemove)));
assertThat(itemChannelLinkRegistry.getAll(),
hasSize(BULK_ITEM_COUNT * BULK_THING_COUNT * BULK_CHANNEL_COUNT - 1));
}
@Test
public void assertThatAllLinksForThingCanBeDeleted() {
fillRegistryForBulkTests();
ThingUID thingToRemove = new ThingUID(BULK_BASE_THING_UID + "_0_0");
int removed = itemChannelLinkRegistry.removeLinksForThing(thingToRemove);
assertThat(removed, is(BULK_CHANNEL_COUNT));
assertThat(itemChannelLinkRegistry.stream().map(ItemChannelLink::getLinkedUID).map(ChannelUID::getThingUID)
.collect(Collectors.toList()), not(hasItem(thingToRemove)));
assertThat(itemChannelLinkRegistry.getAll(),
hasSize((BULK_ITEM_COUNT * BULK_THING_COUNT - 1) * BULK_CHANNEL_COUNT));
}
@Test
public void assertThatCompressOnlyRemovesInvalidLinks() {
fillRegistryForBulkTests();
int expected = BULK_ITEM_COUNT * BULK_THING_COUNT * BULK_CHANNEL_COUNT;
int removed = itemChannelLinkRegistry.purge();
assertThat(removed, is(0));
assertThat(itemChannelLinkRegistry.getAll(), hasSize(expected));
managedItemProvider.remove(BULK_BASE_ITEM_NAME + "_0_0_0");
removed = itemChannelLinkRegistry.purge();
expected -= removed;
assertThat(removed, is(1));
assertThat(itemChannelLinkRegistry.getAll(), hasSize(expected));
managedThingProvider.remove(new ThingUID(BULK_BASE_THING_UID + "_1_0"));
removed = itemChannelLinkRegistry.purge();
expected -= removed;
assertThat(removed, is(BULK_CHANNEL_COUNT));
assertThat(itemChannelLinkRegistry.getAll(), hasSize(expected));
managedItemProvider.remove(BULK_BASE_ITEM_NAME + "_2_0_0");
managedThingProvider.remove(new ThingUID(BULK_BASE_THING_UID + "_2_0"));
removed = itemChannelLinkRegistry.purge();
expected -= removed;
assertThat(removed, is(BULK_CHANNEL_COUNT));
assertThat(itemChannelLinkRegistry.getAll(), hasSize(expected));
}
private void fillRegistryForBulkTests() {
// clear all old links and things
managedItemChannelLinkProvider.getAll().forEach(it -> managedItemChannelLinkProvider.remove(it.getUID()));
managedThingProvider.getAll().forEach(it -> managedThingProvider.remove(it.getUID()));
for (int i = 0; i < BULK_ITEM_COUNT; i++) {
for (int j = 0; j < BULK_THING_COUNT; j++) {
ThingUID thingUID = new ThingUID(BULK_BASE_THING_UID + "_" + i + "_" + j);
ThingBuilder thingBuilder = ThingBuilder.create(THING_TYPE_UID, thingUID);
List<ItemChannelLink> links = new ArrayList<>();
for (int k = 0; k < BULK_CHANNEL_COUNT; k++) {
String itemName = BULK_BASE_ITEM_NAME + "_" + i + "_" + j + "_" + k;
managedItemProvider.add(new ColorItem(itemName));
ChannelUID channelUID = new ChannelUID(thingUID, "channel" + k);
thingBuilder.withChannel(ChannelBuilder.create(channelUID, CoreItemFactory.COLOR).build());
links.add(new ItemChannelLink(itemName, channelUID));
}
managedThingProvider.add(thingBuilder.build());
links.forEach(managedItemChannelLinkProvider::add);
}
}
waitForAssert(() -> assertThat(itemChannelLinkRegistry.getAll(),
hasSize(BULK_ITEM_COUNT * BULK_THING_COUNT * BULK_CHANNEL_COUNT)));
assertThat(managedThingProvider.getAll(), hasSize(BULK_ITEM_COUNT * BULK_THING_COUNT));
}
} }